/*-
* See the file LICENSE for redistribution information.
*
* Copyright (c) 2000,2008 Oracle. All rights reserved.
*
* $Id: ForeignKeyTest.java,v 1.1 2008/02/07 17:12:32 mark Exp $
*/
package com.sleepycat.persist.test;
import static com.sleepycat.persist.model.DeleteAction.ABORT;
import static com.sleepycat.persist.model.DeleteAction.CASCADE;
import static com.sleepycat.persist.model.DeleteAction.NULLIFY;
import static com.sleepycat.persist.model.Relationship.ONE_TO_ONE;
import java.util.Enumeration;
import junit.framework.Test;
import junit.framework.TestSuite;
import com.sleepycat.db.DatabaseException;
import com.sleepycat.db.Transaction;
import com.sleepycat.persist.EntityStore;
import com.sleepycat.persist.PrimaryIndex;
import com.sleepycat.persist.SecondaryIndex;
import com.sleepycat.persist.StoreConfig;
import com.sleepycat.persist.model.DeleteAction;
import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.Persistent;
import com.sleepycat.persist.model.PrimaryKey;
import com.sleepycat.persist.model.SecondaryKey;
import com.sleepycat.util.test.TxnTestCase;
/**
* @author Mark Hayes
*/
public class ForeignKeyTest extends TxnTestCase {
private static final DeleteAction[] ACTIONS = {
ABORT,
NULLIFY,
CASCADE,
};
private static final String[] ACTION_LABELS = {
"ABORT",
"NULLIFY",
"CASCADE",
};
public static Test suite() {
TestSuite suite = new TestSuite();
for (int i = 0; i < ACTIONS.length; i += 1) {
for (int j = 0; j < 2; j++) {
TestSuite txnSuite = txnTestSuite
(ForeignKeyTest.class, null, null);
Enumeration e = txnSuite.tests();
while (e.hasMoreElements()) {
ForeignKeyTest test = (ForeignKeyTest) e.nextElement();
test.onDelete = ACTIONS[i];
test.onDeleteLabel = ACTION_LABELS[i];
test.useSubclass = (j == 0);
test.useSubclassLabel =
(j == 0) ? "UseSubclass" : "UseBaseclass";
suite.addTest(test);
}
}
}
return suite;
}
private EntityStore store;
private PrimaryIndex<String,Entity1> pri1;
private PrimaryIndex<String,Entity2> pri2;
private SecondaryIndex<String,String,Entity1> sec1;
private SecondaryIndex<String,String,Entity2> sec2;
private DeleteAction onDelete;
private String onDeleteLabel;
private boolean useSubclass;
private String useSubclassLabel;
public void tearDown()
throws Exception {
super.tearDown();
setName(getName() + '-' + onDeleteLabel + "-" + useSubclassLabel);
}
private void open()
throws DatabaseException {
StoreConfig config = new StoreConfig();
config.setAllowCreate(envConfig.getAllowCreate());
config.setTransactional(envConfig.getTransactional());
store = new EntityStore(env, "test", config);
pri1 = store.getPrimaryIndex(String.class, Entity1.class);
sec1 = store.getSecondaryIndex(pri1, String.class, "sk");
pri2 = store.getPrimaryIndex(String.class, Entity2.class);
sec2 = store.getSecondaryIndex
(pri2, String.class, "sk_" + onDeleteLabel);
}
private void close()
throws DatabaseException {
store.close();
}
public void testForeignKeys()
throws Exception {
open();
Transaction txn = txnBegin();
Entity1 o1 = new Entity1("pk1", "sk1");
assertNull(pri1.put(txn, o1));
assertEquals(o1, pri1.get(txn, "pk1", null));
assertEquals(o1, sec1.get(txn, "sk1", null));
Entity2 o2 = (useSubclass ?
new Entity3("pk2", "pk1", onDelete) :
new Entity2("pk2", "pk1", onDelete));
assertNull(pri2.put(txn, o2));
assertEquals(o2, pri2.get(txn, "pk2", null));
assertEquals(o2, sec2.get(txn, "pk1", null));
txnCommit(txn);
txn = txnBegin();
/*
* pri1 contains o1 with primary key "pk1" and index key "sk1".
*
* pri2 contains o2 with primary key "pk2" and foreign key "pk1",
* which is the primary key of pri1.
*/
if (onDelete == ABORT) {
/* Test that we abort trying to delete a referenced key. */
try {
pri1.delete(txn, "pk1");
fail();
} catch (DatabaseException expected) {
txnAbort(txn);
txn = txnBegin();
}
/*
* Test that we can put a record into store2 with a null foreign
* key value.
*/
o2 = (useSubclass ?
new Entity3("pk2", null, onDelete) :
new Entity2("pk2", null, onDelete));
assertNotNull(pri2.put(txn, o2));
assertEquals(o2, pri2.get(txn, "pk2", null));
/*
* The index2 record should have been deleted since the key was set
* to null above.
*/
assertNull(sec2.get(txn, "pk1", null));
/*
* Test that now we can delete the record in store1, since it is no
* longer referenced.
*/
assertNotNull(pri1.delete(txn, "pk1"));
assertNull(pri1.get(txn, "pk1", null));
assertNull(sec1.get(txn, "sk1", null));
} else if (onDelete == NULLIFY) {
/* Delete the referenced key. */
assertNotNull(pri1.delete(txn, "pk1"));
assertNull(pri1.get(txn, "pk1", null));
assertNull(sec1.get(txn, "sk1", null));
/*
* The store2 record should still exist, but should have an empty
* secondary key since it was nullified.
*/
o2 = pri2.get(txn, "pk2", null);
assertNotNull(o2);
assertEquals("pk2", o2.pk);
assertEquals(null, o2.getSk(onDelete));
} else if (onDelete == CASCADE) {
/* Delete the referenced key. */
assertNotNull(pri1.delete(txn, "pk1"));
assertNull(pri1.get(txn, "pk1", null));
assertNull(sec1.get(txn, "sk1", null));
/* The store2 record should have deleted also. */
assertNull(pri2.get(txn, "pk2", null));
assertNull(sec2.get(txn, "pk1", null));
} else {
throw new IllegalStateException();
}
/*
* Test that a foreign key value may not be used that is not present in
* the foreign store. "pk2" is not in store1 in this case.
*/
Entity2 o3 = (useSubclass ?
new Entity3("pk3", "pk2", onDelete) :
new Entity2("pk3", "pk2", onDelete));
try {
pri2.put(txn, o3);
fail();
} catch (DatabaseException expected) {
}
txnCommit(txn);
close();
}
@Entity
static class Entity1 {
@PrimaryKey
String pk;
@SecondaryKey(relate=ONE_TO_ONE)
String sk;
private Entity1() {}
Entity1(String pk, String sk) {
this.pk = pk;
this.sk = sk;
}
@Override
public boolean equals(Object other) {
Entity1 o = (Entity1) other;
return nullOrEqual(pk, o.pk) &&
nullOrEqual(sk, o.sk);
}
}
@Entity
static class Entity2 {
@PrimaryKey
String pk;
@SecondaryKey(relate=ONE_TO_ONE, relatedEntity=Entity1.class,
onRelatedEntityDelete=ABORT)
String sk_ABORT;
@SecondaryKey(relate=ONE_TO_ONE, relatedEntity=Entity1.class,
onRelatedEntityDelete=CASCADE)
String sk_CASCADE;
@SecondaryKey(relate=ONE_TO_ONE, relatedEntity=Entity1.class,
onRelatedEntityDelete=NULLIFY)
String sk_NULLIFY;
private Entity2() {}
Entity2(String pk, String sk, DeleteAction action) {
this.pk = pk;
switch (action) {
case ABORT:
sk_ABORT = sk;
break;
case CASCADE:
sk_CASCADE = sk;
break;
case NULLIFY:
sk_NULLIFY = sk;
break;
default:
throw new IllegalArgumentException();
}
}
String getSk(DeleteAction action) {
switch (action) {
case ABORT:
return sk_ABORT;
case CASCADE:
return sk_CASCADE;
case NULLIFY:
return sk_NULLIFY;
default:
throw new IllegalArgumentException();
}
}
@Override
public boolean equals(Object other) {
Entity2 o = (Entity2) other;
return nullOrEqual(pk, o.pk) &&
nullOrEqual(sk_ABORT, o.sk_ABORT) &&
nullOrEqual(sk_CASCADE, o.sk_CASCADE) &&
nullOrEqual(sk_NULLIFY, o.sk_NULLIFY);
}
}
@Persistent
static class Entity3 extends Entity2 {
Entity3() {}
Entity3(String pk, String sk, DeleteAction action) {
super(pk, sk, action);
}
}
static boolean nullOrEqual(Object o1, Object o2) {
if (o1 == null) {
return o2 == null;
} else {
return o1.equals(o2);
}
}
}