/*-
* See the file LICENSE for redistribution information.
*
* Copyright (c) 2000, 2015 Oracle and/or its affiliates. All rights reserved.
*
*/
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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import com.sleepycat.compat.DbCompat;
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
*/
@RunWith(Parameterized.class)
public class ForeignKeyTest extends TxnTestCase {
protected static final DeleteAction[] ACTIONS = {
ABORT,
NULLIFY,
CASCADE,
};
protected static final String[] ACTION_LABELS = {
"ABORT",
"NULLIFY",
"CASCADE",
};
@Parameters
public static List<Object[]> genParams() {
return paramsHelper(false);
}
protected static List<Object[]> paramsHelper(boolean rep) {
final String[] txnTypes = getTxnTypes(null, rep);
final List<Object[]> newParams = new ArrayList<Object[]>();
int i = 0;
for (final DeleteAction action : ACTIONS) {
for (final String type : txnTypes) {
newParams.add(new Object[]
{type, action, ACTION_LABELS[i], "UseSubclass"});
newParams.add(new Object[]
{type, action, ACTION_LABELS[i], "UseBaseclass"});
}
i++;
}
return newParams;
}
public ForeignKeyTest(String type,
DeleteAction action,
String label,
String useClassLabel){
initEnvConfig();
txnType = type;
isTransactional = (txnType != TXN_NULL);
onDelete = action;
onDeleteLabel = label;
useSubclassLabel = useClassLabel;
customName = txnType + '-' + onDeleteLabel + "-" + useSubclassLabel;
}
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 final DeleteAction onDelete;
private final String onDeleteLabel;
private boolean useSubclass;
private final String 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();
}
@Test
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) {
assertTrue(!DbCompat.NEW_JE_EXCEPTIONS);
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) {
assertTrue(!DbCompat.NEW_JE_EXCEPTIONS);
}
txnAbort(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);
}
}
}