/*- * See the file LICENSE for redistribution information. * * Copyright (c) 2002, 2015 Oracle and/or its affiliates. All rights reserved. * */ package com.sleepycat.persist.test; import static com.sleepycat.persist.model.DeleteAction.CASCADE; import static com.sleepycat.persist.model.DeleteAction.NULLIFY; import static com.sleepycat.persist.model.Relationship.MANY_TO_MANY; import static com.sleepycat.persist.model.Relationship.MANY_TO_ONE; import static com.sleepycat.persist.model.Relationship.ONE_TO_MANY; 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.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.junit.After; 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.Database; import com.sleepycat.db.DatabaseConfig; import com.sleepycat.db.DatabaseException; import com.sleepycat.db.StatsConfig; import com.sleepycat.db.Transaction; import com.sleepycat.persist.EntityCursor; import com.sleepycat.persist.EntityIndex; import com.sleepycat.persist.EntityStore; import com.sleepycat.persist.PrimaryIndex; import com.sleepycat.persist.SecondaryIndex; import com.sleepycat.persist.StoreConfig; import com.sleepycat.persist.impl.Store; import com.sleepycat.persist.model.Entity; import com.sleepycat.persist.model.KeyField; import com.sleepycat.persist.model.NotPersistent; import com.sleepycat.persist.model.NotTransient; import com.sleepycat.persist.model.Persistent; import com.sleepycat.persist.model.PrimaryKey; import com.sleepycat.persist.model.SecondaryKey; import com.sleepycat.persist.raw.RawStore; import com.sleepycat.util.test.TxnTestCase; /** * Tests misc store and index operations that are not tested by IndexTest. * * @author Mark Hayes */ @RunWith(Parameterized.class) public class OperationTest extends TxnTestCase { private static final String STORE_NAME = "test"; @Parameters public static List<Object[]> genParams() { return getTxnParams(null, false); } public OperationTest(String type){ initEnvConfig(); txnType = type; isTransactional = (txnType != TXN_NULL); customName = txnType; } private EntityStore store; private void openReadOnly() throws DatabaseException { StoreConfig config = new StoreConfig(); config.setReadOnly(true); open(config); } private void open() throws DatabaseException { open((Class) null); } private void open(Class clsToRegister) throws DatabaseException { StoreConfig config = new StoreConfig(); config.setAllowCreate(envConfig.getAllowCreate()); if (clsToRegister != null) { com.sleepycat.persist.model.EntityModel model = new com.sleepycat.persist.model.AnnotationModel(); model.registerClass(clsToRegister); config.setModel(model); } open(config); } private void open(StoreConfig config) throws DatabaseException { config.setTransactional(envConfig.getTransactional()); store = new EntityStore(env, STORE_NAME, config); } private void close() throws DatabaseException { store.close(); store = null; } /** * The store must be closed before closing the environment. */ @After public void tearDown() throws Exception { try { if (store != null) { store.close(); } } catch (Throwable e) { System.out.println("During tearDown: " + e); } store = null; super.tearDown(); } @Test public void testReadOnly() throws DatabaseException { open(); PrimaryIndex<Integer, SharedSequenceEntity1> priIndex = store.getPrimaryIndex(Integer.class, SharedSequenceEntity1.class); Transaction txn = txnBegin(); SharedSequenceEntity1 e = new SharedSequenceEntity1(); priIndex.put(txn, e); assertEquals(1, e.key); txnCommit(txn); close(); /* * Check that we can open the store read-only and read the records * written above. */ openReadOnly(); priIndex = store.getPrimaryIndex(Integer.class, SharedSequenceEntity1.class); e = priIndex.get(1); assertNotNull(e); close(); } @Test public void testUninitializedCursor() throws DatabaseException { open(); PrimaryIndex<Integer, MyEntity> priIndex = store.getPrimaryIndex(Integer.class, MyEntity.class); Transaction txn = txnBeginCursor(); MyEntity e = new MyEntity(); e.priKey = 1; e.secKey = 1; priIndex.put(txn, e); EntityCursor<MyEntity> entities = priIndex.entities(txn, getWriteCursorConfig()); try { entities.nextDup(); fail(); } catch (IllegalStateException expected) {} try { entities.prevDup(); fail(); } catch (IllegalStateException expected) {} try { entities.current(); fail(); } catch (IllegalStateException expected) {} try { entities.delete(); fail(); } catch (IllegalStateException expected) {} try { entities.update(e); fail(); } catch (IllegalStateException expected) {} try { entities.count(); fail(); } catch (IllegalStateException expected) {} entities.close(); txnCommit(txn); close(); } @Test public void testCursorCount() throws DatabaseException { open(); PrimaryIndex<Integer, MyEntity> priIndex = store.getPrimaryIndex(Integer.class, MyEntity.class); SecondaryIndex<Integer, Integer, MyEntity> secIndex = store.getSecondaryIndex(priIndex, Integer.class, "secKey"); Transaction txn = txnBeginCursor(); MyEntity e = new MyEntity(); e.priKey = 1; e.secKey = 1; priIndex.put(txn, e); EntityCursor<MyEntity> cursor = secIndex.entities(txn, null); cursor.next(); assertEquals(1, cursor.count()); cursor.close(); e.priKey = 2; priIndex.put(txn, e); cursor = secIndex.entities(txn, null); cursor.next(); assertEquals(2, cursor.count()); cursor.close(); txnCommit(txn); close(); } @Test public void testCursorUpdate() throws DatabaseException { open(); PrimaryIndex<Integer, MyEntity> priIndex = store.getPrimaryIndex(Integer.class, MyEntity.class); SecondaryIndex<Integer, Integer, MyEntity> secIndex = store.getSecondaryIndex(priIndex, Integer.class, "secKey"); Transaction txn = txnBeginCursor(); Integer k; MyEntity e = new MyEntity(); e.priKey = 1; e.secKey = 2; priIndex.put(txn, e); /* update() with primary entity cursor. */ EntityCursor<MyEntity> entities = priIndex.entities(txn, getWriteCursorConfig()); e = entities.next(); assertNotNull(e); assertEquals(1, e.priKey); assertEquals(Integer.valueOf(2), e.secKey); e.secKey = null; assertTrue(entities.update(e)); e = entities.current(); assertNotNull(e); assertEquals(1, e.priKey); assertEquals(null, e.secKey); e.secKey = 3; assertTrue(entities.update(e)); e = entities.current(); assertNotNull(e); assertEquals(1, e.priKey); assertEquals(Integer.valueOf(3), e.secKey); entities.close(); /* update() with primary keys cursor. */ EntityCursor<Integer> keys = priIndex.keys(txn, getWriteCursorConfig()); k = keys.next(); assertNotNull(k); assertEquals(Integer.valueOf(1), k); try { keys.update(2); fail(); } catch (UnsupportedOperationException expected) { } keys.close(); /* update() with secondary entity cursor. */ entities = secIndex.entities(txn, null); e = entities.next(); assertNotNull(e); assertEquals(1, e.priKey); assertEquals(Integer.valueOf(3), e.secKey); try { entities.update(e); fail(); } catch (UnsupportedOperationException expected) { } catch (IllegalArgumentException expectedForDbCore) { } entities.close(); /* update() with secondary keys cursor. */ keys = secIndex.keys(txn, null); k = keys.next(); assertNotNull(k); assertEquals(Integer.valueOf(3), k); try { keys.update(k); fail(); } catch (UnsupportedOperationException expected) { } keys.close(); txnCommit(txn); close(); } @Test public void testCursorDelete() throws DatabaseException { open(); PrimaryIndex<Integer, MyEntity> priIndex = store.getPrimaryIndex(Integer.class, MyEntity.class); SecondaryIndex<Integer, Integer, MyEntity> secIndex = store.getSecondaryIndex(priIndex, Integer.class, "secKey"); Transaction txn = txnBeginCursor(); /* delete() with primary and secondary entities cursor. */ for (EntityIndex index : new EntityIndex[] { priIndex, secIndex }) { MyEntity e = new MyEntity(); e.priKey = 1; e.secKey = 1; priIndex.put(txn, e); e.priKey = 2; priIndex.put(txn, e); EntityCursor<MyEntity> cursor = index.entities(txn, getWriteCursorConfig()); e = cursor.next(); assertNotNull(e); assertEquals(1, e.priKey); e = cursor.current(); assertNotNull(e); assertEquals(1, e.priKey); assertTrue(cursor.delete()); assertTrue(!cursor.delete()); assertNull(cursor.current()); e = cursor.next(); assertNotNull(e); assertEquals(2, e.priKey); e = cursor.current(); assertNotNull(e); assertEquals(2, e.priKey); assertTrue(cursor.delete()); assertTrue(!cursor.delete()); assertNull(cursor.current()); e = cursor.next(); assertNull(e); if (index == priIndex) { e = new MyEntity(); e.priKey = 2; e.secKey = 1; assertTrue(!cursor.update(e)); } cursor.close(); } /* delete() with primary and secondary keys cursor. */ for (EntityIndex index : new EntityIndex[] { priIndex, secIndex }) { MyEntity e = new MyEntity(); e.priKey = 1; e.secKey = 1; priIndex.put(txn, e); e.priKey = 2; priIndex.put(txn, e); EntityCursor<Integer> cursor = index.keys(txn, getWriteCursorConfig()); Integer k = cursor.next(); assertNotNull(k); assertEquals(1, k.intValue()); k = cursor.current(); assertNotNull(k); assertEquals(1, k.intValue()); assertTrue(cursor.delete()); assertTrue(!cursor.delete()); assertNull(cursor.current()); int expectKey = (index == priIndex) ? 2 : 1; k = cursor.next(); assertNotNull(k); assertEquals(expectKey, k.intValue()); k = cursor.current(); assertNotNull(k); assertEquals(expectKey, k.intValue()); assertTrue(cursor.delete()); assertTrue(!cursor.delete()); assertNull(cursor.current()); k = cursor.next(); assertNull(k); cursor.close(); } txnCommit(txn); close(); } @Test public void testDeleteFromSubIndex() throws DatabaseException { open(); PrimaryIndex<Integer, MyEntity> priIndex = store.getPrimaryIndex(Integer.class, MyEntity.class); SecondaryIndex<Integer, Integer, MyEntity> secIndex = store.getSecondaryIndex(priIndex, Integer.class, "secKey"); Transaction txn = txnBegin(); MyEntity e = new MyEntity(); e.secKey = 1; e.priKey = 1; priIndex.put(txn, e); e.priKey = 2; priIndex.put(txn, e); e.priKey = 3; priIndex.put(txn, e); e.priKey = 4; priIndex.put(txn, e); txnCommit(txn); EntityIndex<Integer, MyEntity> subIndex = secIndex.subIndex(1); txn = txnBeginCursor(); e = subIndex.get(txn, 1, null); assertEquals(1, e.priKey); assertEquals(Integer.valueOf(1), e.secKey); e = subIndex.get(txn, 2, null); assertEquals(2, e.priKey); assertEquals(Integer.valueOf(1), e.secKey); e = subIndex.get(txn, 3, null); assertEquals(3, e.priKey); assertEquals(Integer.valueOf(1), e.secKey); e = subIndex.get(txn, 5, null); assertNull(e); boolean deleted = subIndex.delete(txn, 1); assertTrue(deleted); assertNull(subIndex.get(txn, 1, null)); assertNotNull(subIndex.get(txn, 2, null)); EntityCursor<MyEntity> cursor = subIndex.entities(txn, getWriteCursorConfig()); boolean saw4 = false; for (MyEntity e2 = cursor.first(); e2 != null; e2 = cursor.next()) { if (e2.priKey == 3) { cursor.delete(); } if (e2.priKey == 4) { saw4 = true; } } cursor.close(); assertTrue(saw4); assertNull(subIndex.get(txn, 1, null)); assertNull(subIndex.get(txn, 3, null)); assertNotNull(subIndex.get(txn, 2, null)); assertNotNull(subIndex.get(txn, 4, null)); txnCommit(txn); close(); } @Entity static class MyEntity { @PrimaryKey private int priKey; @SecondaryKey(relate=MANY_TO_ONE) private Integer secKey; private MyEntity() {} } @Test public void testSharedSequence() throws DatabaseException { open(); PrimaryIndex<Integer, SharedSequenceEntity1> priIndex1 = store.getPrimaryIndex(Integer.class, SharedSequenceEntity1.class); PrimaryIndex<Integer, SharedSequenceEntity2> priIndex2 = store.getPrimaryIndex(Integer.class, SharedSequenceEntity2.class); Transaction txn = txnBegin(); SharedSequenceEntity1 e1 = new SharedSequenceEntity1(); SharedSequenceEntity2 e2 = new SharedSequenceEntity2(); priIndex1.put(txn, e1); assertEquals(1, e1.key); priIndex2.putNoOverwrite(txn, e2); assertEquals(Integer.valueOf(2), e2.key); e1.key = 0; priIndex1.putNoOverwrite(txn, e1); assertEquals(3, e1.key); e2.key = null; priIndex2.put(txn, e2); assertEquals(Integer.valueOf(4), e2.key); txnCommit(txn); close(); } @Entity static class SharedSequenceEntity1 { @PrimaryKey(sequence="shared") private int key; } @Entity static class SharedSequenceEntity2 { @PrimaryKey(sequence="shared") private Integer key; } @Test public void testSeparateSequence() throws DatabaseException { open(); PrimaryIndex<Integer, SeparateSequenceEntity1> priIndex1 = store.getPrimaryIndex (Integer.class, SeparateSequenceEntity1.class); PrimaryIndex<Integer, SeparateSequenceEntity2> priIndex2 = store.getPrimaryIndex (Integer.class, SeparateSequenceEntity2.class); Transaction txn = txnBegin(); SeparateSequenceEntity1 e1 = new SeparateSequenceEntity1(); SeparateSequenceEntity2 e2 = new SeparateSequenceEntity2(); priIndex1.put(txn, e1); assertEquals(1, e1.key); priIndex2.putNoOverwrite(txn, e2); assertEquals(Integer.valueOf(1), e2.key); e1.key = 0; priIndex1.putNoOverwrite(txn, e1); assertEquals(2, e1.key); e2.key = null; priIndex2.put(txn, e2); assertEquals(Integer.valueOf(2), e2.key); txnCommit(txn); close(); } @Entity static class SeparateSequenceEntity1 { @PrimaryKey(sequence="seq1") private int key; } @Entity static class SeparateSequenceEntity2 { @PrimaryKey(sequence="seq2") private Integer key; } @Test public void testCompositeSequence() throws DatabaseException { open(); PrimaryIndex<CompositeSequenceEntity1.Key, CompositeSequenceEntity1> priIndex1 = store.getPrimaryIndex (CompositeSequenceEntity1.Key.class, CompositeSequenceEntity1.class); PrimaryIndex<CompositeSequenceEntity2.Key, CompositeSequenceEntity2> priIndex2 = store.getPrimaryIndex (CompositeSequenceEntity2.Key.class, CompositeSequenceEntity2.class); Transaction txn = txnBegin(); CompositeSequenceEntity1 e1 = new CompositeSequenceEntity1(); CompositeSequenceEntity2 e2 = new CompositeSequenceEntity2(); priIndex1.put(txn, e1); assertEquals(1, e1.key.key); priIndex2.putNoOverwrite(txn, e2); assertEquals(Integer.valueOf(1), e2.key.key); e1.key = null; priIndex1.putNoOverwrite(txn, e1); assertEquals(2, e1.key.key); e2.key = null; priIndex2.put(txn, e2); assertEquals(Integer.valueOf(2), e2.key.key); txnCommit(txn); txn = txnBeginCursor(); EntityCursor<CompositeSequenceEntity1> c1 = priIndex1.entities(txn, null); e1 = c1.next(); assertEquals(2, e1.key.key); e1 = c1.next(); assertEquals(1, e1.key.key); e1 = c1.next(); assertNull(e1); c1.close(); txnCommit(txn); txn = txnBeginCursor(); EntityCursor<CompositeSequenceEntity2> c2 = priIndex2.entities(txn, null); e2 = c2.next(); assertEquals(Integer.valueOf(2), e2.key.key); e2 = c2.next(); assertEquals(Integer.valueOf(1), e2.key.key); e2 = c2.next(); assertNull(e2); c2.close(); txnCommit(txn); close(); } @Entity static class CompositeSequenceEntity1 { @Persistent static class Key implements Comparable<Key> { @KeyField(1) private int key; public int compareTo(Key o) { /* Reverse the natural order. */ return o.key - key; } } @PrimaryKey(sequence="seq1") private Key key; } /** * Same as CompositeSequenceEntity1 but using Integer rather than int for * the key type. */ @Entity static class CompositeSequenceEntity2 { @Persistent static class Key implements Comparable<Key> { @KeyField(1) private Integer key; public int compareTo(Key o) { /* Reverse the natural order. */ return o.key - key; } } @PrimaryKey(sequence="seq2") private Key key; } /** * When opening read-only, secondaries are not opened when the primary is * opened, causing a different code path to be used for opening * secondaries. For a RawStore in particular, this caused an unreported * NullPointerException in JE 3.0.12. No SR was created because the use * case is very obscure and was discovered by code inspection. */ @Test public void testOpenRawStoreReadOnly() throws DatabaseException { open(); store.getPrimaryIndex(Integer.class, MyEntity.class); close(); StoreConfig config = new StoreConfig(); config.setReadOnly(true); config.setTransactional(envConfig.getTransactional()); RawStore rawStore = new RawStore(env, "test", config); String clsName = MyEntity.class.getName(); rawStore.getSecondaryIndex(clsName, "secKey"); rawStore.close(); } /** * When opening an X_TO_MANY secondary that has a persistent key class, the * key class was not recognized as being persistent if it was never before * referenced when getSecondaryIndex was called. This was a bug in JE * 3.0.12, reported on OTN. [#15103] */ @Test public void testToManyKeyClass() throws DatabaseException { open(); PrimaryIndex<Integer, ToManyKeyEntity> priIndex = store.getPrimaryIndex(Integer.class, ToManyKeyEntity.class); SecondaryIndex<ToManyKey, Integer, ToManyKeyEntity> secIndex = store.getSecondaryIndex(priIndex, ToManyKey.class, "key2"); priIndex.put(new ToManyKeyEntity()); secIndex.get(new ToManyKey()); close(); } /** * Test a fix for a bug where opening a TO_MANY secondary index would fail * fail with "IllegalArgumentException: Wrong secondary key class: ..." * when the store was opened read-only. [#15156] */ @Test public void testToManyReadOnly() throws DatabaseException { open(); PrimaryIndex<Integer, ToManyKeyEntity> priIndex = store.getPrimaryIndex(Integer.class, ToManyKeyEntity.class); priIndex.put(new ToManyKeyEntity()); close(); openReadOnly(); priIndex = store.getPrimaryIndex(Integer.class, ToManyKeyEntity.class); SecondaryIndex<ToManyKey, Integer, ToManyKeyEntity> secIndex = store.getSecondaryIndex(priIndex, ToManyKey.class, "key2"); secIndex.get(new ToManyKey()); close(); } @Persistent static class ToManyKey { @KeyField(1) int value = 99; } @Entity static class ToManyKeyEntity { @PrimaryKey int key = 88; @SecondaryKey(relate=ONE_TO_MANY) Set<ToManyKey> key2; ToManyKeyEntity() { key2 = new HashSet<ToManyKey>(); key2.add(new ToManyKey()); } } /** * When Y is opened and X has a key with relatedEntity=Y.class, X should * be opened automatically. If X is not opened, foreign key constraints * will not be enforced. [#15358] */ @Test public void testAutoOpenRelatedEntity() throws DatabaseException { PrimaryIndex<Integer, RelatedY> priY; PrimaryIndex<Integer, RelatedX> priX; /* Opening X should create (and open) Y and enforce constraints. */ open(); priX = store.getPrimaryIndex(Integer.class, RelatedX.class); PersistTestUtils.assertDbExists (true, env, STORE_NAME, RelatedY.class.getName(), null); if (isTransactional) { /* Constraint enforcement requires transactions. */ try { priX.put(new RelatedX()); fail(); } catch (DatabaseException e) { assertTrue ("" + e.getMessage(), (e.getMessage().indexOf ("foreign key not allowed: it is not present") >= 0) || (e.getMessage().indexOf("DB_FOREIGN_CONFLICT") >= 0)); } } priY = store.getPrimaryIndex(Integer.class, RelatedY.class); priY.put(new RelatedY()); priX.put(new RelatedX()); close(); /* Delete should cascade even when X is not opened explicitly. */ open(); priY = store.getPrimaryIndex(Integer.class, RelatedY.class); assertEquals(1, priY.count()); priY.delete(88); assertEquals(0, priY.count()); priX = store.getPrimaryIndex(Integer.class, RelatedX.class); assertEquals(0, priX.count()); /* Failed prior to [#15358] fix. */ close(); } @Entity static class RelatedX { @PrimaryKey int key = 99; @SecondaryKey(relate=ONE_TO_ONE, relatedEntity=RelatedY.class, onRelatedEntityDelete=CASCADE) int key2 = 88; RelatedX() { } } @Entity static class RelatedY { @PrimaryKey int key = 88; RelatedY() { } } @Test public void testSecondaryBulkLoad1() throws DatabaseException { doSecondaryBulkLoad(true); } @Test public void testSecondaryBulkLoad2() throws DatabaseException { doSecondaryBulkLoad(false); } private void doSecondaryBulkLoad(boolean closeAndOpenNormally) throws DatabaseException { PrimaryIndex<Integer, RelatedX> priX; PrimaryIndex<Integer, RelatedY> priY; SecondaryIndex<Integer, Integer, RelatedX> secX; /* Open priX with SecondaryBulkLoad=true. */ StoreConfig config = new StoreConfig(); config.setAllowCreate(true); config.setSecondaryBulkLoad(true); open(config); /* Getting priX should not create the secondary index. */ priX = store.getPrimaryIndex(Integer.class, RelatedX.class); PersistTestUtils.assertDbExists (false, env, STORE_NAME, RelatedX.class.getName(), "key2"); /* We can put records that violate the secondary key constraint. */ priX.put(new RelatedX()); if (closeAndOpenNormally) { /* Open normally and attempt to populate the secondary. */ close(); open(); if (isTransactional && DbCompat.POPULATE_ENFORCES_CONSTRAINTS) { /* Constraint enforcement requires transactions. */ try { /* Before adding the foreign key, constraint is violated. */ priX = store.getPrimaryIndex(Integer.class, RelatedX.class); fail(); } catch (DatabaseException e) { assertTrue (e.toString(), e.toString().contains("foreign key not allowed")); } } /* Open priX with SecondaryBulkLoad=true. */ close(); open(config); /* Add the foreign key to avoid the constraint error. */ priY = store.getPrimaryIndex(Integer.class, RelatedY.class); priY.put(new RelatedY()); /* Open normally and the secondary will be populated. */ close(); open(); priX = store.getPrimaryIndex(Integer.class, RelatedX.class); PersistTestUtils.assertDbExists (true, env, STORE_NAME, RelatedX.class.getName(), "key2"); secX = store.getSecondaryIndex(priX, Integer.class, "key2"); } else { /* Get secondary index explicitly and it will be populated. */ if (isTransactional && DbCompat.POPULATE_ENFORCES_CONSTRAINTS) { /* Constraint enforcement requires transactions. */ try { /* Before adding the foreign key, constraint is violated. */ secX = store.getSecondaryIndex(priX, Integer.class, "key2"); fail(); } catch (DatabaseException e) { assertTrue (e.toString(), e.toString().contains("foreign key not allowed")); } } /* Add the foreign key. */ priY = store.getPrimaryIndex(Integer.class, RelatedY.class); priY.put(new RelatedY()); secX = store.getSecondaryIndex(priX, Integer.class, "key2"); PersistTestUtils.assertDbExists (true, env, STORE_NAME, RelatedX.class.getName(), "key2"); } RelatedX x = secX.get(88); assertNotNull(x); close(); } @Test public void testPersistentFields() throws DatabaseException { open(); PrimaryIndex<Integer, PersistentFields> pri = store.getPrimaryIndex(Integer.class, PersistentFields.class); PersistentFields o1 = new PersistentFields(-1, 1, 2, 3, 4, 5, 6); assertNull(pri.put(o1)); PersistentFields o2 = pri.get(-1); assertNotNull(o2); assertEquals(0, o2.transient1); assertEquals(0, o2.transient2); assertEquals(0, o2.transient3); assertEquals(4, o2.persistent1); assertEquals(5, o2.persistent2); assertEquals(6, o2.persistent3); close(); } @Entity static class PersistentFields { @PrimaryKey int key; transient int transient1; @NotPersistent int transient2; @NotPersistent transient int transient3; int persistent1; @NotTransient int persistent2; @NotTransient transient int persistent3; PersistentFields(int k, int t1, int t2, int t3, int p1, int p2, int p3) { key = k; transient1 = t1; transient2 = t2; transient3 = t3; persistent1 = p1; persistent2 = p2; persistent3 = p3; } private PersistentFields() {} } /** * When a primary or secondary has a persistent key class, the key class * was not recognized as being persistent when getPrimaryConfig, * getSecondaryConfig, or getSubclassIndex was called, if that key class * was not previously referenced. All three cases are tested by calling * getSecondaryConfig. This was a bug in JE 3.3.69, reported on OTN. * [#16407] */ @Test public void testKeyClassInitialization() throws DatabaseException { open(); store.getSecondaryConfig(ToManyKeyEntity.class, "key2"); close(); } @Test public void testKeyName() throws DatabaseException { open(); PrimaryIndex<Long, BookEntity> pri1 = store.getPrimaryIndex(Long.class, BookEntity.class); PrimaryIndex<Long, AuthorEntity> pri2 = store.getPrimaryIndex(Long.class, AuthorEntity.class); BookEntity book = new BookEntity(); pri1.put(book); AuthorEntity author = new AuthorEntity(); author.bookIds.add(book.bookId); pri2.put(author); close(); open(); pri1 = store.getPrimaryIndex(Long.class, BookEntity.class); pri2 = store.getPrimaryIndex(Long.class, AuthorEntity.class); book = pri1.get(1L); assertNotNull(book); author = pri2.get(1L); assertNotNull(author); close(); } @Entity static class AuthorEntity { @PrimaryKey(sequence="authorSeq") long authorId; @SecondaryKey(relate=MANY_TO_MANY, relatedEntity=BookEntity.class, name="bookId", onRelatedEntityDelete=NULLIFY) Set<Long> bookIds = new HashSet<Long>(); } @Entity static class BookEntity { @PrimaryKey(sequence="bookSeq") long bookId; } /** * Checks that we get an appropriate exception when storing an entity * subclass instance, which contains a secondary key, without registering * the subclass up front. [#16399] */ @Test public void testPutEntitySubclassWithoutRegisterClass() throws DatabaseException { open(); final PrimaryIndex<Long, Statement> pri = store.getPrimaryIndex(Long.class, Statement.class); final Transaction txn = txnBegin(); pri.put(txn, new Statement(1)); try { pri.put(txn, new ExtendedStatement(2, null)); fail(); } catch (IllegalArgumentException expected) { assertTrue(expected.toString(), expected.getMessage().contains ("Entity subclasses defining a secondary key must be " + "registered by calling EntityModel.registerClass or " + "EntityStore.getSubclassIndex before storing an instance " + "of the subclass: " + ExtendedStatement.class.getName())); } txnAbort(txn); close(); } /** * Checks that registerClass avoids an exception when storing an entity * subclass instance, which defines a secondary key. [#16399] */ @Test public void testPutEntitySubclassWithRegisterClass() throws DatabaseException { open(ExtendedStatement.class); final PrimaryIndex<Long, Statement> pri = store.getPrimaryIndex(Long.class, Statement.class); final Transaction txn = txnBegin(); pri.put(txn, new Statement(1)); pri.put(txn, new ExtendedStatement(2, "abc")); txnCommit(txn); final SecondaryIndex<String, Long, ExtendedStatement> sec = store.getSubclassIndex(pri, ExtendedStatement.class, String.class, "name"); ExtendedStatement o = sec.get("abc"); assertNotNull(o); assertEquals(2, o.id); close(); } /** * Same as testPutEntitySubclassWithRegisterClass but store the first * instance of the subclass after closing and reopening the store, * *without* calling registerClass. This ensures that a single call to * registerClass is sufficient and subsequent use of the store does not * require it. [#16399] */ @Test public void testPutEntitySubclassWithRegisterClass2() throws DatabaseException { open(ExtendedStatement.class); PrimaryIndex<Long, Statement> pri = store.getPrimaryIndex(Long.class, Statement.class); Transaction txn = txnBegin(); pri.put(txn, new Statement(1)); txnCommit(txn); close(); open(); pri = store.getPrimaryIndex(Long.class, Statement.class); txn = txnBegin(); pri.put(txn, new ExtendedStatement(2, "abc")); txnCommit(txn); final SecondaryIndex<String, Long, ExtendedStatement> sec = store.getSubclassIndex(pri, ExtendedStatement.class, String.class, "name"); ExtendedStatement o = sec.get("abc"); assertNotNull(o); assertEquals(2, o.id); close(); } /** * Checks that getSubclassIndex can be used instead of registerClass to * avoid an exception when storing an entity subclass instance, which * defines a secondary key. [#16399] */ @Test public void testPutEntitySubclassWithGetSubclassIndex() throws DatabaseException { open(); final PrimaryIndex<Long, Statement> pri = store.getPrimaryIndex(Long.class, Statement.class); final SecondaryIndex<String, Long, ExtendedStatement> sec = store.getSubclassIndex(pri, ExtendedStatement.class, String.class, "name"); final Transaction txn = txnBegin(); pri.put(txn, new Statement(1)); pri.put(txn, new ExtendedStatement(2, "abc")); txnCommit(txn); ExtendedStatement o = sec.get("abc"); assertNotNull(o); assertEquals(2, o.id); close(); } /** * Same as testPutEntitySubclassWithGetSubclassIndex2 but store the first * instance of the subclass after closing and reopening the store, * *without* calling getSubclassIndex. This ensures that a single call to * getSubclassIndex is sufficient and subsequent use of the store does not * require it. [#16399] */ @Test public void testPutEntitySubclassWithGetSubclassIndex2() throws DatabaseException { open(); PrimaryIndex<Long, Statement> pri = store.getPrimaryIndex(Long.class, Statement.class); SecondaryIndex<String, Long, ExtendedStatement> sec = store.getSubclassIndex(pri, ExtendedStatement.class, String.class, "name"); Transaction txn = txnBegin(); pri.put(txn, new Statement(1)); txnCommit(txn); close(); open(); pri = store.getPrimaryIndex(Long.class, Statement.class); txn = txnBegin(); pri.put(txn, new ExtendedStatement(2, "abc")); txnCommit(txn); sec = store.getSubclassIndex(pri, ExtendedStatement.class, String.class, "name"); ExtendedStatement o = sec.get("abc"); assertNotNull(o); assertEquals(2, o.id); close(); } /** * Checks that secondary population occurs only once when an index is * created, not every time it is opened, even when it is empty. This is a * JE-only test because we don't have a portable way to get stats that * indicate whether primary reads were performed. [#16399] */ @Entity static class Statement { @PrimaryKey long id; Statement(long id) { this.id = id; } private Statement() {} } @Persistent static class ExtendedStatement extends Statement { @SecondaryKey(relate=MANY_TO_ONE) String name; ExtendedStatement(long id, String name) { super(id); this.name = name; } private ExtendedStatement() {} } @Test public void testCustomCompare() throws DatabaseException { open(); PrimaryIndex<ReverseIntKey, CustomCompareEntity> priIndex = store.getPrimaryIndex (ReverseIntKey.class, CustomCompareEntity.class); SecondaryIndex<ReverseIntKey, ReverseIntKey, CustomCompareEntity> secIndex1 = store.getSecondaryIndex(priIndex, ReverseIntKey.class, "secKey1"); SecondaryIndex<ReverseIntKey, ReverseIntKey, CustomCompareEntity> secIndex2 = store.getSecondaryIndex(priIndex, ReverseIntKey.class, "secKey2"); Transaction txn = txnBegin(); for (int i = 1; i <= 5; i += 1) { assertTrue(priIndex.putNoOverwrite(txn, new CustomCompareEntity(i))); } txnCommit(txn); txn = txnBeginCursor(); EntityCursor<CustomCompareEntity> c = priIndex.entities(txn, null); for (int i = 5; i >= 1; i -= 1) { CustomCompareEntity e = c.next(); assertNotNull(e); assertEquals(new ReverseIntKey(i), e.key); } c.close(); txnCommit(txn); txn = txnBeginCursor(); c = secIndex1.entities(txn, null); for (int i = -1; i >= -5; i -= 1) { CustomCompareEntity e = c.next(); assertNotNull(e); assertEquals(new ReverseIntKey(-i), e.key); assertEquals(new ReverseIntKey(i), e.secKey1); } c.close(); txnCommit(txn); txn = txnBeginCursor(); c = secIndex2.entities(txn, null); for (int i = -1; i >= -5; i -= 1) { CustomCompareEntity e = c.next(); assertNotNull(e); assertEquals(new ReverseIntKey(-i), e.key); assertTrue(e.secKey2.contains(new ReverseIntKey(i))); } c.close(); txnCommit(txn); close(); } @Entity static class CustomCompareEntity { @PrimaryKey private ReverseIntKey key; @SecondaryKey(relate=MANY_TO_ONE) private ReverseIntKey secKey1; @SecondaryKey(relate=ONE_TO_MANY) private final Set<ReverseIntKey> secKey2 = new HashSet<ReverseIntKey>(); private CustomCompareEntity() {} CustomCompareEntity(int i) { key = new ReverseIntKey(i); secKey1 = new ReverseIntKey(-i); secKey2.add(new ReverseIntKey(-i)); } } @Persistent static class ReverseIntKey implements Comparable<ReverseIntKey> { @KeyField(1) private int key; public int compareTo(ReverseIntKey o) { /* Reverse the natural order. */ return o.key - key; } private ReverseIntKey() {} ReverseIntKey(int key) { this.key = key; } @Override public boolean equals(Object o) { return key == ((ReverseIntKey) o).key; } @Override public int hashCode() { return key; } @Override public String toString() { return "Key = " + key; } } /** * Ensures that custom comparators are persisted and work correctly during * recovery. JE recovery uses comparators, so they are serialized and * stored in the DatabaseImpl. They are deserialized during recovery prior * to opening the EntityStore and its format catalog. But the formats are * needed by the comparator, so they are specially created when needed. * * In particular we need to ensure that enum key fields work correctly, * since their formats are not static (like simple type formats are). * [#17140] * * Note that we don't need to actually cause a recovery in order to test * the deserialization and subsequent use of comparators. The JE * DatabaseConfig.setBtreeComparator method serializes and deserializes the * comparator. The comparator is initialized on its first use, just as if * recovery were run. */ @Test public void testStoredComparators() throws DatabaseException { open(); PrimaryIndex<StoredComparatorEntity.Key, StoredComparatorEntity> priIndex = store.getPrimaryIndex(StoredComparatorEntity.Key.class, StoredComparatorEntity.class); SecondaryIndex<StoredComparatorEntity.MyEnum, StoredComparatorEntity.Key, StoredComparatorEntity> secIndex = store.getSecondaryIndex (priIndex, StoredComparatorEntity.MyEnum.class, "secKey"); final StoredComparatorEntity.Key[] priKeys = new StoredComparatorEntity.Key[] { new StoredComparatorEntity.Key (StoredComparatorEntity.MyEnum.A, 1, StoredComparatorEntity.MyEnum.A), new StoredComparatorEntity.Key (StoredComparatorEntity.MyEnum.A, 1, StoredComparatorEntity.MyEnum.B), new StoredComparatorEntity.Key (StoredComparatorEntity.MyEnum.A, 2, StoredComparatorEntity.MyEnum.A), new StoredComparatorEntity.Key (StoredComparatorEntity.MyEnum.A, 2, StoredComparatorEntity.MyEnum.B), new StoredComparatorEntity.Key (StoredComparatorEntity.MyEnum.B, 1, StoredComparatorEntity.MyEnum.A), new StoredComparatorEntity.Key (StoredComparatorEntity.MyEnum.B, 1, StoredComparatorEntity.MyEnum.B), new StoredComparatorEntity.Key (StoredComparatorEntity.MyEnum.C, 0, StoredComparatorEntity.MyEnum.C), }; final StoredComparatorEntity.MyEnum[] secKeys = new StoredComparatorEntity.MyEnum[] { StoredComparatorEntity.MyEnum.C, StoredComparatorEntity.MyEnum.B, StoredComparatorEntity.MyEnum.A, null, StoredComparatorEntity.MyEnum.A, StoredComparatorEntity.MyEnum.B, StoredComparatorEntity.MyEnum.C, }; assertEquals(priKeys.length, secKeys.length); final int nEntities = priKeys.length; Transaction txn = txnBegin(); for (int i = 0; i < nEntities; i += 1) { priIndex.put(txn, new StoredComparatorEntity(priKeys[i], secKeys[i])); } txnCommit(txn); txn = txnBeginCursor(); EntityCursor<StoredComparatorEntity> entities = priIndex.entities(txn, null); for (int i = nEntities - 1; i >= 0; i -= 1) { StoredComparatorEntity e = entities.next(); assertNotNull(e); assertEquals(priKeys[i], e.key); assertEquals(secKeys[i], e.secKey); } assertNull(entities.next()); entities.close(); txnCommit(txn); txn = txnBeginCursor(); entities = secIndex.entities(txn, null); for (StoredComparatorEntity.MyEnum myEnum : EnumSet.allOf(StoredComparatorEntity.MyEnum.class)) { for (int i = nEntities - 1; i >= 0; i -= 1) { if (secKeys[i] == myEnum) { StoredComparatorEntity e = entities.next(); assertNotNull(e); assertEquals(priKeys[i], e.key); assertEquals(secKeys[i], e.secKey); } } } assertNull(entities.next()); entities.close(); txnCommit(txn); close(); } @Entity static class StoredComparatorEntity { enum MyEnum { A, B, C }; @Persistent static class Key implements Comparable<Key> { @KeyField(1) MyEnum f1; @KeyField(2) Integer f2; @KeyField(3) MyEnum f3; private Key() {} Key(MyEnum f1, Integer f2, MyEnum f3) { this.f1 = f1; this.f2 = f2; this.f3 = f3; } public int compareTo(Key o) { /* Reverse the natural order. */ int i = f1.compareTo(o.f1); if (i != 0) return -i; i = f2.compareTo(o.f2); if (i != 0) return -i; i = f3.compareTo(o.f3); if (i != 0) return -i; return 0; } @Override public boolean equals(Object other) { if (!(other instanceof Key)) { return false; } Key o = (Key) other; return f1 == o.f1 && f2.equals(o.f2) && f3 == o.f3; } @Override public int hashCode() { return f1.ordinal() + f2 + f3.ordinal(); } @Override public String toString() { return "[Key " + f1 + ' ' + f2 + ' ' + f3 + ']'; } } @PrimaryKey Key key; @SecondaryKey(relate=MANY_TO_ONE) private MyEnum secKey; private StoredComparatorEntity() {} StoredComparatorEntity(Key key, MyEnum secKey) { this.key = key; this.secKey = secKey; } @Override public String toString() { return "[pri = " + key + " sec = " + secKey + ']'; } } @Test public void testEmbeddedMapTypes() throws DatabaseException { open(); PrimaryIndex<Integer, EmbeddedMapTypes> pri = store.getPrimaryIndex(Integer.class, EmbeddedMapTypes.class); pri.put(null, new EmbeddedMapTypes()); close(); open(); pri = store.getPrimaryIndex(Integer.class, EmbeddedMapTypes.class); EmbeddedMapTypes entity = pri.get(1); assertNotNull(entity); EmbeddedMapTypes entity2 = new EmbeddedMapTypes(); assertEquals(entity.getF1(), entity2.getF1()); close(); } enum MyEnum { ONE, TWO }; @Entity static class EmbeddedMapTypes { @PrimaryKey private final int f0 = 1; private final Map<MyEnum, HashMap<MyEnum, MyEnum>> f1; EmbeddedMapTypes() { f1 = new HashMap<MyEnum, HashMap<MyEnum, MyEnum>>(); HashMap<MyEnum, MyEnum> f2 = new HashMap<MyEnum, MyEnum>(); f2.put(MyEnum.ONE, MyEnum.ONE); f1.put(MyEnum.ONE, f2); f2 = new HashMap<MyEnum, MyEnum>(); f2.put(MyEnum.TWO, MyEnum.TWO); f1.put(MyEnum.TWO, f2); } public int getPriKey() { return f0; } public Map<MyEnum, HashMap<MyEnum, MyEnum>> getF1() { return f1; } } }