1 /*-
2  * Copyright (c) 2000, 2020 Oracle and/or its affiliates.  All rights reserved.
3  *
4  * See the file LICENSE for license information.
5  *
6  */
7 
8 package com.sleepycat.persist.test;
9 
10 import static com.sleepycat.persist.model.DeleteAction.ABORT;
11 import static com.sleepycat.persist.model.DeleteAction.CASCADE;
12 import static com.sleepycat.persist.model.DeleteAction.NULLIFY;
13 import static com.sleepycat.persist.model.Relationship.ONE_TO_ONE;
14 import static org.junit.Assert.assertEquals;
15 import static org.junit.Assert.assertNotNull;
16 import static org.junit.Assert.assertNull;
17 import static org.junit.Assert.assertTrue;
18 import static org.junit.Assert.fail;
19 
20 import java.util.ArrayList;
21 import java.util.List;
22 
23 import org.junit.Test;
24 import org.junit.runner.RunWith;
25 import org.junit.runners.Parameterized;
26 import org.junit.runners.Parameterized.Parameters;
27 
28 import com.sleepycat.compat.DbCompat;
29 import com.sleepycat.db.DatabaseException;
30 import com.sleepycat.db.Transaction;
31 import com.sleepycat.persist.EntityStore;
32 import com.sleepycat.persist.PrimaryIndex;
33 import com.sleepycat.persist.SecondaryIndex;
34 import com.sleepycat.persist.StoreConfig;
35 import com.sleepycat.persist.model.DeleteAction;
36 import com.sleepycat.persist.model.Entity;
37 import com.sleepycat.persist.model.Persistent;
38 import com.sleepycat.persist.model.PrimaryKey;
39 import com.sleepycat.persist.model.SecondaryKey;
40 import com.sleepycat.util.test.TxnTestCase;
41 
42 /**
43  * @author Mark Hayes
44  */
45 @RunWith(Parameterized.class)
46 public class ForeignKeyTest extends TxnTestCase {
47 
48     protected static final DeleteAction[] ACTIONS = {
49         ABORT,
50         NULLIFY,
51         CASCADE,
52     };
53 
54     protected static final String[] ACTION_LABELS = {
55         "ABORT",
56         "NULLIFY",
57         "CASCADE",
58     };
59 
60     @Parameters
genParams()61     public static List<Object[]> genParams() {
62         return paramsHelper(false);
63     }
64 
paramsHelper(boolean rep)65     protected static List<Object[]> paramsHelper(boolean rep) {
66         final String[] txnTypes = getTxnTypes(null, rep);
67         final List<Object[]> newParams = new ArrayList<Object[]>();
68         int i = 0;
69         for (final DeleteAction action : ACTIONS) {
70             for (final String type : txnTypes) {
71                 newParams.add(new Object[]
72                     {type, action, ACTION_LABELS[i], "UseSubclass"});
73                 newParams.add(new Object[]
74                     {type, action, ACTION_LABELS[i], "UseBaseclass"});
75             }
76             i++;
77         }
78         return newParams;
79     }
80 
ForeignKeyTest(String type, DeleteAction action, String label, String useClassLabel)81     public ForeignKeyTest(String type,
82                           DeleteAction action,
83                           String label,
84                           String useClassLabel){
85         initEnvConfig();
86         txnType = type;
87         isTransactional = (txnType != TXN_NULL);
88         onDelete = action;
89         onDeleteLabel = label;
90         useSubclassLabel = useClassLabel;
91         customName = txnType + '-' + onDeleteLabel + "-" + useSubclassLabel;
92     }
93 
94     private EntityStore store;
95     private PrimaryIndex<String, Entity1> pri1;
96     private PrimaryIndex<String, Entity2> pri2;
97     private SecondaryIndex<String, String, Entity1> sec1;
98     private SecondaryIndex<String, String, Entity2> sec2;
99     private final DeleteAction onDelete;
100     private final String onDeleteLabel;
101     private boolean useSubclass;
102     private final String useSubclassLabel;
103 
open()104     private void open()
105         throws DatabaseException {
106 
107         StoreConfig config = new StoreConfig();
108         config.setAllowCreate(envConfig.getAllowCreate());
109         config.setTransactional(envConfig.getTransactional());
110 
111         store = new EntityStore(env, "test", config);
112 
113         pri1 = store.getPrimaryIndex(String.class, Entity1.class);
114         sec1 = store.getSecondaryIndex(pri1, String.class, "sk");
115         pri2 = store.getPrimaryIndex(String.class, Entity2.class);
116         sec2 = store.getSecondaryIndex
117             (pri2, String.class, "sk_" + onDeleteLabel);
118     }
119 
close()120     private void close()
121         throws DatabaseException {
122 
123         store.close();
124     }
125 
126     @Test
testForeignKeys()127     public void testForeignKeys()
128         throws Exception {
129 
130         open();
131         Transaction txn = txnBegin();
132 
133         Entity1 o1 = new Entity1("pk1", "sk1");
134         assertNull(pri1.put(txn, o1));
135 
136         assertEquals(o1, pri1.get(txn, "pk1", null));
137         assertEquals(o1, sec1.get(txn, "sk1", null));
138 
139         Entity2 o2 = (useSubclass ?
140                       new Entity3("pk2", "pk1", onDelete) :
141                       new Entity2("pk2", "pk1", onDelete));
142         assertNull(pri2.put(txn, o2));
143 
144         assertEquals(o2, pri2.get(txn, "pk2", null));
145         assertEquals(o2, sec2.get(txn, "pk1", null));
146 
147         txnCommit(txn);
148         txn = txnBegin();
149 
150         /*
151          * pri1 contains o1 with primary key "pk1" and index key "sk1".
152          *
153          * pri2 contains o2 with primary key "pk2" and foreign key "pk1",
154          * which is the primary key of pri1.
155          */
156         if (onDelete == ABORT) {
157 
158             /* Test that we abort trying to delete a referenced key. */
159 
160             try {
161                 pri1.delete(txn, "pk1");
162                 fail();
163             } catch (DatabaseException expected) {
164                 assertTrue(!DbCompat.NEW_JE_EXCEPTIONS);
165                 txnAbort(txn);
166                 txn = txnBegin();
167             }
168 
169             /*
170              * Test that we can put a record into store2 with a null foreign
171              * key value.
172              */
173             o2 = (useSubclass ?
174                   new Entity3("pk2", null, onDelete) :
175                   new Entity2("pk2", null, onDelete));
176             assertNotNull(pri2.put(txn, o2));
177             assertEquals(o2, pri2.get(txn, "pk2", null));
178 
179             /*
180              * The index2 record should have been deleted since the key was set
181              * to null above.
182              */
183             assertNull(sec2.get(txn, "pk1", null));
184 
185             /*
186              * Test that now we can delete the record in store1, since it is no
187              * longer referenced.
188              */
189             assertNotNull(pri1.delete(txn, "pk1"));
190             assertNull(pri1.get(txn, "pk1", null));
191             assertNull(sec1.get(txn, "sk1", null));
192 
193         } else if (onDelete == NULLIFY) {
194 
195             /* Delete the referenced key. */
196             assertNotNull(pri1.delete(txn, "pk1"));
197             assertNull(pri1.get(txn, "pk1", null));
198             assertNull(sec1.get(txn, "sk1", null));
199 
200             /*
201              * The store2 record should still exist, but should have an empty
202              * secondary key since it was nullified.
203              */
204             o2 = pri2.get(txn, "pk2", null);
205             assertNotNull(o2);
206             assertEquals("pk2", o2.pk);
207             assertEquals(null, o2.getSk(onDelete));
208 
209         } else if (onDelete == CASCADE) {
210 
211             /* Delete the referenced key. */
212             assertNotNull(pri1.delete(txn, "pk1"));
213             assertNull(pri1.get(txn, "pk1", null));
214             assertNull(sec1.get(txn, "sk1", null));
215 
216             /* The store2 record should have deleted also. */
217             assertNull(pri2.get(txn, "pk2", null));
218             assertNull(sec2.get(txn, "pk1", null));
219 
220         } else {
221             throw new IllegalStateException();
222         }
223 
224         /*
225          * Test that a foreign key value may not be used that is not present in
226          * the foreign store. "pk2" is not in store1 in this case.
227          */
228         Entity2 o3 = (useSubclass ?
229                       new Entity3("pk3", "pk2", onDelete) :
230                       new Entity2("pk3", "pk2", onDelete));
231         try {
232             pri2.put(txn, o3);
233             fail();
234         } catch (DatabaseException expected) {
235             assertTrue(!DbCompat.NEW_JE_EXCEPTIONS);
236         }
237 
238         txnAbort(txn);
239         close();
240     }
241 
242     @Entity
243     static class Entity1 {
244 
245         @PrimaryKey
246         String pk;
247 
248         @SecondaryKey(relate=ONE_TO_ONE)
249         String sk;
250 
Entity1()251         private Entity1() {}
252 
Entity1(String pk, String sk)253         Entity1(String pk, String sk) {
254             this.pk = pk;
255             this.sk = sk;
256         }
257 
258         @Override
equals(Object other)259         public boolean equals(Object other) {
260             Entity1 o = (Entity1) other;
261             return nullOrEqual(pk, o.pk) &&
262                    nullOrEqual(sk, o.sk);
263         }
264     }
265 
266     @Entity
267     static class Entity2 {
268 
269         @PrimaryKey
270         String pk;
271 
272         @SecondaryKey(relate=ONE_TO_ONE, relatedEntity=Entity1.class,
273                                          onRelatedEntityDelete=ABORT)
274         String sk_ABORT;
275 
276         @SecondaryKey(relate=ONE_TO_ONE, relatedEntity=Entity1.class,
277                                          onRelatedEntityDelete=CASCADE)
278         String sk_CASCADE;
279 
280         @SecondaryKey(relate=ONE_TO_ONE, relatedEntity=Entity1.class,
281                                          onRelatedEntityDelete=NULLIFY)
282         String sk_NULLIFY;
283 
Entity2()284         private Entity2() {}
285 
Entity2(String pk, String sk, DeleteAction action)286         Entity2(String pk, String sk, DeleteAction action) {
287             this.pk = pk;
288             switch (action) {
289             case ABORT:
290                 sk_ABORT = sk;
291                 break;
292             case CASCADE:
293                 sk_CASCADE = sk;
294                 break;
295             case NULLIFY:
296                 sk_NULLIFY = sk;
297                 break;
298             default:
299                 throw new IllegalArgumentException();
300             }
301         }
302 
getSk(DeleteAction action)303         String getSk(DeleteAction action) {
304             switch (action) {
305             case ABORT:
306                 return sk_ABORT;
307             case CASCADE:
308                 return sk_CASCADE;
309             case NULLIFY:
310                 return sk_NULLIFY;
311             default:
312                 throw new IllegalArgumentException();
313             }
314         }
315 
316         @Override
equals(Object other)317         public boolean equals(Object other) {
318             Entity2 o = (Entity2) other;
319             return nullOrEqual(pk, o.pk) &&
320                    nullOrEqual(sk_ABORT, o.sk_ABORT) &&
321                    nullOrEqual(sk_CASCADE, o.sk_CASCADE) &&
322                    nullOrEqual(sk_NULLIFY, o.sk_NULLIFY);
323         }
324     }
325 
326     @Persistent
327     static class Entity3 extends Entity2 {
Entity3()328         Entity3() {}
329 
Entity3(String pk, String sk, DeleteAction action)330         Entity3(String pk, String sk, DeleteAction action) {
331             super(pk, sk, action);
332         }
333     }
334 
nullOrEqual(Object o1, Object o2)335     static boolean nullOrEqual(Object o1, Object o2) {
336         if (o1 == null) {
337             return o2 == null;
338         } else {
339             return o1.equals(o2);
340         }
341     }
342 }
343