1 /*-
2  * See the file LICENSE for redistribution information.
3  *
4  * Copyright (c) 2002, 2014 Oracle and/or its affiliates.  All rights reserved.
5  *
6  */
7 
8 package com.sleepycat.je;
9 
10 import static com.sleepycat.je.dbi.DbiStatDefinition.THROUGHPUT_SECONDARYDB_DELETE;
11 import static com.sleepycat.je.dbi.DbiStatDefinition.THROUGHPUT_SECONDARYDB_GET;
12 import static com.sleepycat.je.dbi.DbiStatDefinition.THROUGHPUT_SECONDARYDB_GETSEARCHBOTH;
13 
14 import java.util.Collection;
15 import java.util.Collections;
16 import java.util.HashSet;
17 import java.util.Iterator;
18 import java.util.List;
19 import java.util.Set;
20 import java.util.logging.Level;
21 
22 import com.sleepycat.je.dbi.CursorImpl.SearchMode;
23 import com.sleepycat.je.dbi.DatabaseImpl;
24 import com.sleepycat.je.dbi.EnvironmentImpl;
25 import com.sleepycat.je.dbi.GetMode;
26 import com.sleepycat.je.dbi.PutMode;
27 import com.sleepycat.je.txn.Locker;
28 import com.sleepycat.je.txn.LockerFactory;
29 import com.sleepycat.je.utilint.AtomicLongStat;
30 import com.sleepycat.je.utilint.DatabaseUtil;
31 import com.sleepycat.je.utilint.LoggerUtils;
32 
33 /**
34  * A secondary database handle.
35  *
36  * <p>Secondary databases are opened with {@link
37  * Environment#openSecondaryDatabase Environment.openSecondaryDatabase} and are
38  * always associated with a single primary database.  The distinguishing
39  * characteristics of a secondary database are:</p>
40  *
41  * <ul> <li>Records are automatically added to a secondary database when
42  * records are added, modified and deleted in the primary database.  Direct
43  * calls to <code>put()</code> methods on a secondary database are
44  * prohibited.</li>
45  * <li>The {@link #delete delete} method of a secondary database will delete
46  * the primary record and as well as all its associated secondary records.</li>
47  * <li>Calls to all <code>get()</code> methods will return the data from the
48  * associated primary database.</li>
49  * <li>Additional <code>get()</code> method signatures are provided to return
50  * the primary key in an additional <code>pKey</code> parameter.</li>
51  * <li>Calls to {@link #openCursor openCursor} will return a {@link
52  * SecondaryCursor}, which itself has <code>get()</code> methods that return
53  * the data of the primary database and additional <code>get()</code> method
54  * signatures for returning the primary key.</li>
55  * </ul>
56  * <p>Before opening or creating a secondary database you must implement
57  * the {@link SecondaryKeyCreator} or {@link SecondaryMultiKeyCreator}
58  * interface.</p>
59  *
60  * <p>For example, to create a secondary database that supports duplicates:</p>
61  *
62  * <pre>
63  *     Database primaryDb; // The primary database must already be open.
64  *     SecondaryKeyCreator keyCreator; // Your key creator implementation.
65  *     SecondaryConfig secConfig = new SecondaryConfig();
66  *     secConfig.setAllowCreate(true);
67  *     secConfig.setSortedDuplicates(true);
68  *     secConfig.setKeyCreator(keyCreator);
69  *     SecondaryDatabase newDb = env.openSecondaryDatabase(transaction,
70  *                                                         "myDatabaseName",
71  *                                                         primaryDb,
72  *                                                         secConfig)
73  * </pre>
74  *
75  * <p>If a primary database is to be associated with one or more secondary
76  * databases, it may not be configured for duplicates.</p>
77  *
78  * <p><b>WARNING:</b> The associations between primary and secondary databases
79  * are not stored persistently.  Whenever a primary database is opened for
80  * write access by the application, the appropriate associated secondary
81  * databases should also be opened by the application.  This is necessary to
82  * ensure data integrity when changes are made to the primary database.  If the
83  * secondary database is not opened, it will not be updated when the primary is
84  * updated, and the references between the databases will become invalid.
85  * (Note that this warning does not apply when using the {@link
86  * com.sleepycat.persist DPL}, which does store secondary relationships
87  * persistently.)</p>
88  *
89  * <h3><a name="transactions">Special considerations for using Secondary
90  * Databases with and without Transactions</a></h3>
91  *
92  * <p>Normally, during a primary database write operation (insert, update or
93  * delete), all associated secondary databases are also updated.  However, when
94  * an exception occurs during the write operation, the updates may be
95  * incomplete.  If the databases are transactional, this is handled by aborting
96  * the transaction to undo the incomplete operation.  If an auto-commit
97  * transaction is used (null is passed for the transaction), the transaction
98  * will be aborted automatically.  If an explicit transaction is used, it
99  * must be aborted by the application caller after the exception is caught.</p>
100  *
101  * <p>However, if the databases are non-transactional, integrity problems can
102  * result when an exception occurs during the write operation.  Because the
103  * write operation is not made atomic by a transaction, references between the
104  * databases will become invalid if the operation is incomplete.  This results
105  * in a {@link SecondaryIntegrityException} when attempting to access the
106  * databases later.</p>
107  *
108  * <p>A secondary integrity problem is persistent; it cannot be resolved by
109  * reopening the databases or the environment.  The only way to resolve the
110  * problem is to restore the environment from a valid backup, or, if the
111  * integrity of the primary database is assumed, to remove and recreate all
112  * secondary databases.</p>
113  *
114  * <p>Therefore, secondary databases and indexes should always be used in
115  * conjunction with transactional databases and stores. Without transactions,
116  * it is the responsibility of the application to handle the results of the
117  * incomplete write operation or to take steps to prevent this situation from
118  * happening in the first place.</p>
119  *
120  * <p>The following exceptions may be thrown during a write operation, and may
121  * cause an integrity problem in the absence of transactions.</p>
122  * <ul>
123  * <li>{@link SecondaryConstraintException}, see its subclasses for more
124  * information.</li>
125  * <li>{@link LockConflictException}, when more than one thread is accessing
126  * the databases.</li>
127  * <li>{@link EnvironmentFailureException}, if an unexpected or system failure
128  * occurs.</li>
129  * <li>There is always the possibility of an {@link Error} or an unintended
130  * {@link RuntimeException}.</li>
131  * </ul>
132  */
133 public class SecondaryDatabase extends Database {
134 
135     /* For type-safe check against EMPTY_SET */
136     private static final Set<DatabaseEntry> EMPTY_SET =
137         Collections.emptySet();
138 
139     private final Database primaryDatabase; // May be null.
140     private SecondaryConfig secondaryConfig;
141     private volatile boolean isFullyPopulated = true;
142 
143     private AtomicLongStat deleteStat;
144     private AtomicLongStat getStat;
145     private AtomicLongStat getSearchBothStat;
146 
147     /**
148      * Creates a secondary database but does not open or fully initialize it.
149      *
150      * @throws IllegalArgumentException via Environment.openSecondaryDatabase.
151      */
SecondaryDatabase(final Environment env, final SecondaryConfig secConfig, final Database primaryDatabase)152     SecondaryDatabase(final Environment env,
153                       final SecondaryConfig secConfig,
154                       final Database primaryDatabase)
155         throws DatabaseException {
156 
157         super(env);
158         this.primaryDatabase = primaryDatabase;
159         if (primaryDatabase == null) {
160             if (secConfig.getSecondaryAssociation() == null) {
161                 throw new IllegalArgumentException(
162                     "Exactly one must be non-null: " +
163                     "PrimaryDatabase or SecondaryAssociation");
164             }
165             if (secConfig.getAllowPopulate()) {
166                 throw new IllegalArgumentException(
167                     "AllowPopulate must be false when a SecondaryAssociation" +
168                     " is configured");
169             }
170         } else {
171             if (secConfig.getSecondaryAssociation() != null) {
172                 throw new IllegalArgumentException(
173                     "Exactly one must be non-null: " +
174                     "PrimaryDatabase or SecondaryAssociation");
175             }
176             primaryDatabase.checkOpen("Can't use as primary:");
177             if (primaryDatabase.configuration.getSortedDuplicates()) {
178                 throw new IllegalArgumentException(
179                     "Duplicates not allowed for a primary database: " +
180                     primaryDatabase.getDebugName());
181             }
182             if (env.getEnvironmentImpl() !=
183                     primaryDatabase.getEnvironment().getEnvironmentImpl()) {
184                 throw new IllegalArgumentException(
185                     "Primary and secondary databases must be in the same" +
186                     " environment");
187             }
188             if (!primaryDatabase.configuration.getReadOnly() &&
189                 secConfig.getKeyCreator() == null &&
190                 secConfig.getMultiKeyCreator() == null) {
191                 throw new IllegalArgumentException(
192                     "SecondaryConfig.getKeyCreator()/getMultiKeyCreator()" +
193                     " may be null only if the primary database is read-only");
194             }
195         }
196         if (secConfig.getKeyCreator() != null &&
197             secConfig.getMultiKeyCreator() != null) {
198             throw new IllegalArgumentException(
199                 "secConfig.getKeyCreator() and getMultiKeyCreator() may not" +
200                 " both be non-null");
201         }
202         if (secConfig.getForeignKeyNullifier() != null &&
203             secConfig.getForeignMultiKeyNullifier() != null) {
204             throw new IllegalArgumentException(
205                 "secConfig.getForeignKeyNullifier() and" +
206                 " getForeignMultiKeyNullifier() may not both be non-null");
207         }
208         if (secConfig.getForeignKeyDeleteAction() ==
209                          ForeignKeyDeleteAction.NULLIFY &&
210             secConfig.getForeignKeyNullifier() == null &&
211             secConfig.getForeignMultiKeyNullifier() == null) {
212             throw new IllegalArgumentException(
213                 "ForeignKeyNullifier or ForeignMultiKeyNullifier must be" +
214                 " non-null when ForeignKeyDeleteAction is NULLIFY");
215         }
216         if (secConfig.getForeignKeyNullifier() != null &&
217             secConfig.getMultiKeyCreator() != null) {
218             throw new IllegalArgumentException(
219                 "ForeignKeyNullifier may not be used with" +
220                 " SecondaryMultiKeyCreator -- use" +
221                 " ForeignMultiKeyNullifier instead");
222         }
223         if (secConfig.getForeignKeyDatabase() != null) {
224             Database foreignDb = secConfig.getForeignKeyDatabase();
225             if (foreignDb.getDatabaseImpl().getSortedDuplicates()) {
226                 throw new IllegalArgumentException(
227                     "Duplicates must not be allowed for a foreign key " +
228                     " database: " + foreignDb.getDebugName());
229             }
230         }
231         setupThroughputStats(env.getEnvironmentImpl());
232     }
233 
234     /**
235      * Create a database, called by Environment
236      */
237     @Override
initNew(final Environment env, final Locker locker, final String databaseName, final DatabaseConfig dbConfig)238     DatabaseImpl initNew(final Environment env,
239                          final Locker locker,
240                          final String databaseName,
241                          final DatabaseConfig dbConfig)
242         throws DatabaseException {
243 
244         final DatabaseImpl dbImpl =
245             super.initNew(env, locker, databaseName, dbConfig);
246         init(locker);
247         return dbImpl;
248     }
249 
250     /**
251      * Open a database, called by Environment
252      *
253      * @throws IllegalArgumentException via Environment.openSecondaryDatabase.
254      */
255     @Override
initExisting(final Environment env, final Locker locker, final DatabaseImpl database, final String databaseName, final DatabaseConfig dbConfig)256     void initExisting(final Environment env,
257                       final Locker locker,
258                       final DatabaseImpl database,
259                       final String databaseName,
260                       final DatabaseConfig dbConfig)
261         throws DatabaseException {
262 
263         /* Disallow one secondary associated with two different primaries. */
264         if (primaryDatabase != null) {
265             Database otherPriDb = database.findPrimaryDatabase();
266             if (otherPriDb != null &&
267                 otherPriDb.getDatabaseImpl() !=
268                 primaryDatabase.getDatabaseImpl()) {
269                 throw new IllegalArgumentException(
270                     "Secondary already associated with different primary: " +
271                     otherPriDb.getDebugName());
272             }
273         }
274 
275         super.initExisting(env, locker, database, databaseName, dbConfig);
276         init(locker);
277     }
278 
279     /**
280      * Adds secondary to primary's list, and populates the secondary if needed.
281      *
282      * @param locker should be the locker used to open the database.  If a
283      * transactional locker, the population operations will occur in the same
284      * transaction; this may result in a large number of retained locks.  If a
285      * non-transactional locker, the Cursor will create a ThreadLocker (even if
286      * a BasicLocker used for handle locking is passed), and locks will not be
287      * retained.
288      */
init(final Locker locker)289     private void init(final Locker locker)
290         throws DatabaseException {
291 
292         trace(Level.FINEST, "SecondaryDatabase open");
293 
294         secondaryConfig = (SecondaryConfig) configuration;
295 
296         Database foreignDb = secondaryConfig.getForeignKeyDatabase();
297         if (foreignDb != null) {
298             foreignDb.foreignKeySecondaries.add(this);
299         }
300 
301         /* Populate secondary if requested and secondary is empty. */
302         if (!secondaryConfig.getAllowPopulate()) {
303             return;
304         }
305         Cursor secCursor = null;
306         Cursor priCursor = null;
307         try {
308             secCursor = new Cursor(this, locker, null);
309             DatabaseEntry key = new DatabaseEntry();
310             DatabaseEntry data = new DatabaseEntry();
311             OperationStatus status = secCursor.position(key, data,
312                                                         LockMode.DEFAULT,
313                                                         true);
314             if (status != OperationStatus.NOTFOUND) {
315                 return;
316             }
317             /* Is empty, so populate */
318             priCursor = new Cursor(primaryDatabase, locker, null);
319             status = priCursor.position(key, data, LockMode.DEFAULT, true);
320             while (status == OperationStatus.SUCCESS) {
321                 updateSecondary(locker, secCursor, key, null, data);
322                 status = priCursor.retrieveNext(key, data,
323                                                 LockMode.DEFAULT,
324                                                 GetMode.NEXT);
325             }
326         } finally {
327             if (secCursor != null) {
328                 secCursor.close();
329             }
330             if (priCursor != null) {
331                 priCursor.close();
332             }
333         }
334     }
335 
336     @Override
makeSecondaryAssociation()337     SecondaryAssociation makeSecondaryAssociation() {
338         /* Only one is non-null: primaryDatabase, SecondaryAssociation. */
339         if (primaryDatabase != null) {
340             primaryDatabase.simpleAssocSecondaries.add(this);
341             return primaryDatabase.secAssoc;
342         }
343         return configuration.getSecondaryAssociation();
344     }
345 
346     /**
347      * Closes a secondary database and dis-associates it from its primary
348      * database. A secondary database should be closed before closing its
349      * associated primary database.
350      *
351      * {@inheritDoc}
352      *
353      * <!-- inherit other javadoc from overridden method -->
354      */
355     @Override
close()356     public synchronized void close()
357         throws DatabaseException {
358 
359         /* removeReferringAssociations will be called during close. */
360         super.close();
361     }
362 
363     @Override
removeReferringAssociations()364     void removeReferringAssociations() {
365         super.removeReferringAssociations();
366         if (primaryDatabase != null) {
367             primaryDatabase.simpleAssocSecondaries.remove(this);
368         }
369         if (secondaryConfig != null) {
370             final Database foreignDb = secondaryConfig.getForeignKeyDatabase();
371             if (foreignDb != null) {
372                 foreignDb.foreignKeySecondaries.remove(this);
373             }
374         }
375     }
376 
377     /**
378      * @hidden
379      * For internal use only.
380      *
381      * Enables incremental population of this secondary database, so that index
382      * population can occur incrementally, and concurrently with primary
383      * database writes.
384      * <p>
385      * After calling this method (and before calling {@link
386      * #endIncrementalPopulation}), it is expected that the application will
387      * populate the secondary explicitly by calling {@link
388      * Database#populateSecondaries} to process all records for the primary
389      * database(s) associated with this secondary.
390      * <p>
391      * The concurrent population mode supports concurrent indexing by ordinary
392      * writes to the primary database(s) and calls to {@link
393      * Database#populateSecondaries}.  To provide this capability, some
394      * primary-secondary integrity checking is disabled.  The integrity
395      * checking (that is disabled) is meant only to detect application bugs,
396      * and is not necessary for normal operations.  Specifically, the checks
397      * that are disabled are:
398      * <ul>
399      *   <li>When a new secondary key is inserted, because a primary record is
400      *   inserted or updated, we normally check that a key mapped to the
401      *   primary record does not already exist in the secondary database.</li>
402      *   <li>When an existing secondary key is deleted, because a primary
403      *   record is updated or deleted, we normally check that a key mapped to
404      *   the primary record already does exist in the secondary database.</li>
405      * </ul>
406      * Without these checks, one can think of the secondary indexing operations
407      * as being idempotent.  Via the idempotent indexing operations, explicit
408      * population (via {@link Database#populateSecondaries}) and normal
409      * secondary population (via primary writes) collaborate to add and delete
410      * index records as needed.
411      */
startIncrementalPopulation()412     public void startIncrementalPopulation() {
413         isFullyPopulated = false;
414     }
415 
416     /**
417      * @hidden
418      * For internal use only.
419      *
420      * Disables incremental population of this secondary database, after this
421      * index has been fully populated.
422      * <p>
423      * After calling this method, this database may not be populated by calling
424      * {@link Database#populateSecondaries}, and all primary-secondary
425      * integrity checking for this secondary is enabled.
426      */
endIncrementalPopulation()427     public void endIncrementalPopulation() {
428         isFullyPopulated = true;
429     }
430 
431     /**
432      * @hidden
433      * For internal use only.
434      *
435      * @return true if {@link #startIncrementalPopulation} was called, and
436      * {@link #endIncrementalPopulation} was not subsequently called.
437      */
isIncrementalPopulationEnabled()438     public boolean isIncrementalPopulationEnabled() {
439         return !isFullyPopulated;
440     }
441 
442     /**
443      * @hidden
444      * For internal use only.
445      *
446      * Reads {@code batchSize} records starting at the given {@code key} and
447      * {@code data}, and deletes any secondary records having a primary key
448      * (the data of the secondary record) for which {@link
449      * SecondaryAssociation#getPrimary} returns null.  The next key/data pair
450      * to be processed is returned in the {@code key} and {@code data}
451      * parameters so these can be passed in to process the next batch.
452      * <p>
453      * It is the application's responsibility to save the key/data pair
454      * returned by this method, and then pass the saved key/data when the
455      * method is called again to process the next batch of records.  The
456      * application may wish to save the key/data persistently in order to avoid
457      * restarting the processing from the beginning of the database after a
458      * crash.
459      *
460      * @param key contains the starting key for the batch of records to be
461      * processed when this method is called, and contains the next key to be
462      * processed when this method returns.  If {@code key.getData() == null}
463      * when this method is called, the batch will begin with the first record
464      * in the database.
465      *
466      * @param data contains the starting data element (primary key) for the
467      * batch of records to be processed when this method is called, and
468      * contains the next data element to be processed when this method returns.
469      * If {@code key.getData() == null} when this method is called, the batch
470      * will begin with the first record in the database.
471      *
472      * @param batchSize is the maximum number of records to be read, and also
473      * the maximum number of deletions that will be included in a single
474      * transaction.
475      *
476      * @return true if more records may need to be processed, or false if
477      * processing is complete.
478      */
deleteObsoletePrimaryKeys(final DatabaseEntry key, final DatabaseEntry data, final int batchSize)479     public boolean deleteObsoletePrimaryKeys(final DatabaseEntry key,
480                                              final DatabaseEntry data,
481                                              final int batchSize) {
482         try {
483             checkEnv();
484             DatabaseUtil.checkForNullDbt(key, "key", false);
485             if (batchSize <= 0) {
486                 throw new IllegalArgumentException(
487                     "batchSize must be positive");
488             }
489             checkOpen("Can't call deleteObsoletePrimaryKeys:");
490             trace(Level.FINEST, "deleteObsoletePrimaryKeys", null, key,
491                   null, null);
492 
493             final Locker locker = LockerFactory.getWritableLocker(
494                 envHandle, null, getDatabaseImpl().isInternalDb(),
495                 isTransactional(),
496                 getDatabaseImpl().isReplicated() /*autoTxnIsReplicated*/);
497             try {
498                 final Cursor cursor = new Cursor(this, locker, null);
499                 try {
500                     return deleteObsoletePrimaryKeysInternal(
501                         cursor, locker, key, data, batchSize);
502                 } finally {
503                     cursor.close();
504                 }
505             } finally {
506                 locker.operationEnd(true);
507             }
508         } catch (Error E) {
509             DbInternal.getEnvironmentImpl(envHandle).invalidate(E);
510             throw E;
511         }
512     }
513 
514     /**
515      * Use a scan to walk through the primary keys.  If the primary key is
516      * obsolete (SecondaryAssociation.getPrimary returns null), delete the
517      * record.
518      */
deleteObsoletePrimaryKeysInternal(final Cursor cursor, final Locker locker, final DatabaseEntry key, final DatabaseEntry data, final int batchSize)519     private boolean deleteObsoletePrimaryKeysInternal(final Cursor cursor,
520                                                       final Locker locker,
521                                                       final DatabaseEntry key,
522                                                       final DatabaseEntry data,
523                                                       final int batchSize) {
524         /* TODO: use dirty-read scan with mode to return deleted records. */
525         final LockMode scanMode = LockMode.RMW;
526         OperationStatus searchStatus;
527         if (key.getData() == null) {
528             /* Start at first key. */
529             searchStatus = cursor.position(key, data, scanMode, true);
530         } else {
531             /* Resume at key/data pair last processed. */
532             searchStatus = cursor.search(key, data, scanMode,
533                                          SearchMode.BOTH_RANGE);
534             if (searchStatus != OperationStatus.SUCCESS) {
535                 searchStatus = cursor.search(key, data, scanMode,
536                                              SearchMode.SET_RANGE);
537             }
538         }
539         int nProcessed = 0;
540         while (searchStatus == OperationStatus.SUCCESS) {
541             if (nProcessed >= batchSize) {
542                 return true;
543             }
544             nProcessed += 1;
545             if (secAssoc.getPrimary(data) == null) {
546                 cursor.deleteNoNotify(getDatabaseImpl().getRepContext());
547             }
548             searchStatus = cursor.retrieveNext(key, data, scanMode,
549                                                GetMode.NEXT);
550         }
551         return false;
552     }
553 
554     /**
555      * @hidden
556      * For internal use only.
557      */
558     @Override
populateSecondaries(DatabaseEntry key, int batchSize)559     public boolean populateSecondaries(DatabaseEntry key, int batchSize) {
560         throw new UnsupportedOperationException("Not allowed on a secondary");
561     }
562 
563     /**
564      * Returns the primary database associated with this secondary database.
565      *
566      * @return the primary database associated with this secondary database.
567      */
568 
569     /*
570      * To be added when SecondaryAssociation is published:
571      * If a {@link SecondaryAssociation} is {@link
572      * SecondaryCursor#setSecondaryAssociation configured}, this method returns
573      * null.
574      */
getPrimaryDatabase()575     public Database getPrimaryDatabase() {
576         return primaryDatabase;
577     }
578 
579     /**
580      * Returns an empty list, since this database is itself a secondary
581      * database.
582      */
583     @Override
getSecondaryDatabases()584     public List<SecondaryDatabase> getSecondaryDatabases() {
585         return Collections.emptyList();
586     }
587 
588     /**
589      * Returns a copy of the secondary configuration of this database.
590      *
591      * @return a copy of the secondary configuration of this database.
592      *
593      * @throws EnvironmentFailureException if an unexpected, internal or
594      * environment-wide failure occurs.
595      *
596      * @deprecated As of JE 4.0.13, replaced by {@link
597      * SecondaryDatabase#getConfig()}.</p>
598      */
getSecondaryConfig()599     public SecondaryConfig getSecondaryConfig()
600         throws DatabaseException {
601 
602         return getConfig();
603     }
604 
605     /**
606      * Returns a copy of the secondary configuration of this database.
607      *
608      * @return a copy of the secondary configuration of this database.
609      *
610      * @throws EnvironmentFailureException if an unexpected, internal or
611      * environment-wide failure occurs.
612      */
613     @Override
getConfig()614     public SecondaryConfig getConfig()
615         throws DatabaseException {
616 
617         return (SecondaryConfig) super.getConfig();
618     }
619 
620     /**
621      * @hidden
622      * Returns the secondary config without cloning, for internal use.
623      */
getPrivateSecondaryConfig()624     public SecondaryConfig getPrivateSecondaryConfig() {
625         return secondaryConfig;
626     }
627 
628     /**
629      * Obtain a cursor on a database, returning a
630      * <code>SecondaryCursor</code>. Calling this method is the equivalent of
631      * calling {@link #openCursor} and casting the result to {@link
632      * SecondaryCursor}.
633      *
634      * @param txn the transaction used to protect all operations performed with
635      * the cursor, or null if the operations should not be transaction
636      * protected.  If the database is non-transactional, null must be
637      * specified.  For a transactional database, the transaction is optional
638      * for read-only access and required for read-write access.
639      *
640      * @param cursorConfig The cursor attributes.  If null, default attributes
641      * are used.
642      *
643      * @return A secondary database cursor.
644      *
645      * @throws EnvironmentFailureException if an unexpected, internal or
646      * environment-wide failure occurs.
647      *
648      * @deprecated As of JE 4.0.13, replaced by {@link
649      * SecondaryDatabase#openCursor}.</p>
650      */
openSecondaryCursor(final Transaction txn, final CursorConfig cursorConfig)651     public SecondaryCursor openSecondaryCursor(final Transaction txn,
652                                                final CursorConfig cursorConfig)
653         throws DatabaseException {
654 
655         return openCursor(txn, cursorConfig);
656     }
657 
658     /**
659      * Obtain a cursor on a database, returning a <code>SecondaryCursor</code>.
660      */
661     @Override
openCursor(final Transaction txn, final CursorConfig cursorConfig)662     public SecondaryCursor openCursor(final Transaction txn,
663                                       final CursorConfig cursorConfig)
664         throws DatabaseException {
665 
666         checkReadable("Can't call SecondaryDatabase.openCursor:");
667         return (SecondaryCursor) super.openCursor(txn, cursorConfig);
668     }
669 
670     /**
671      * Overrides Database method.
672      */
673     @Override
newDbcInstance(final Transaction txn, final CursorConfig cursorConfig)674     Cursor newDbcInstance(final Transaction txn,
675                           final CursorConfig cursorConfig)
676         throws DatabaseException {
677 
678         return new SecondaryCursor(this, txn, cursorConfig);
679     }
680 
681     /**
682      * Deletes the primary key/data pair associated with the specified
683      * secondary key.  In the presence of duplicate key values, all primary
684      * records associated with the designated secondary key will be deleted.
685      *
686      * When the primary records are deleted, their associated secondary records
687      * are deleted as if {@link Database#delete} were called.  This includes,
688      * but is not limited to, the secondary record referenced by the given key.
689      *
690      * @param key the secondary key used as input.  It must be initialized with
691      * a non-null byte array by the caller.
692      *
693      * <!-- inherit other javadoc from overridden method -->
694      */
695     @Override
delete(final Transaction txn, final DatabaseEntry key)696     public OperationStatus delete(final Transaction txn,
697                                   final DatabaseEntry key)
698         throws DeleteConstraintException,
699                LockConflictException,
700                DatabaseException,
701                UnsupportedOperationException,
702                IllegalArgumentException {
703 
704         checkEnv();
705         DatabaseUtil.checkForNullDbt(key, "key", true);
706         checkReadable("Can't call SecondaryDatabase.delete:");
707         trace(Level.FINEST, "SecondaryDatabase.delete", txn,
708               key, null, null);
709         if (deleteStat != null) {
710             deleteStat.increment();
711         }
712 
713         Locker locker = null;
714         Cursor cursor = null;
715 
716         OperationStatus commitStatus = OperationStatus.NOTFOUND;
717         try {
718             locker = LockerFactory.getWritableLocker(
719                 envHandle,
720                 txn,
721                 getDatabaseImpl().isInternalDb(),
722                 isTransactional(),
723                 getDatabaseImpl().isReplicated()); // autoTxnIsReplicated
724 
725             final LockMode lockMode = locker.isSerializableIsolation() ?
726                 LockMode.RMW :
727                 LockMode.READ_UNCOMMITTED_ALL;
728 
729             /* Read the primary key (the data of a secondary). */
730             cursor = new Cursor(this, locker, null);
731             DatabaseEntry pKey = new DatabaseEntry();
732 
733             OperationStatus searchStatus = cursor.search(
734                 key, pKey, lockMode, SearchMode.SET);
735 
736             /*
737              * For each duplicate secondary key, delete the primary record and
738              * all its associated secondary records, including the one
739              * referenced by this secondary cursor.
740              */
741             while (searchStatus == OperationStatus.SUCCESS) {
742                 final Database primaryDb = getPrimary(pKey);
743                 if (primaryDb == null) {
744                     /* Primary was removed from the association. */
745                     cursor.deleteNoNotify(getDatabaseImpl().getRepContext());
746                 } else {
747                     commitStatus = primaryDb.deleteInternal(locker, pKey);
748                     if (commitStatus != OperationStatus.SUCCESS) {
749                         if (lockMode != LockMode.RMW) {
750 
751                             /*
752                              * The primary record was not found. The index
753                              * may be either corrupt or the record was
754                              * deleted between finding it in the secondary
755                              * without locking and trying to delete it.
756                              * If it was deleted then just skip it.
757                              */
758                             if (cursor.checkCurrent(LockMode.RMW) ==
759                                 OperationStatus.SUCCESS) {
760                                 /* there is a secondary index entry */
761                                 throw secondaryRefersToMissingPrimaryKey(
762                                    locker, key, pKey);
763                             }
764                         } else {
765                             /* there is a secondary index entry */
766                             throw secondaryRefersToMissingPrimaryKey(
767                                locker, key, pKey);
768                         }
769                     }
770                 }
771                 searchStatus = cursor.retrieveNext(
772                     key, pKey, lockMode, GetMode.NEXT_DUP);
773             }
774             return commitStatus;
775         } catch (Error E) {
776             DbInternal.getEnvironmentImpl(envHandle).invalidate(E);
777             throw E;
778         } finally {
779             if (cursor != null) {
780                 cursor.close();
781             }
782             if (locker != null) {
783                 locker.operationEnd(commitStatus);
784             }
785         }
786     }
787 
788     /**
789      * @param key the secondary key used as input.  It must be initialized with
790      * a non-null byte array by the caller.
791      *
792      * @param data the primary data returned as output.  Its byte array does
793      * not need to be initialized by the caller.
794      *
795      * <!-- inherit other javadoc from overridden method -->
796      */
797     @Override
get(final Transaction txn, final DatabaseEntry key, final DatabaseEntry data, final LockMode lockMode)798     public OperationStatus get(final Transaction txn,
799                                final DatabaseEntry key,
800                                final DatabaseEntry data,
801                                final LockMode lockMode)
802         throws DatabaseException {
803 
804         return get(txn, key, new DatabaseEntry(), data, lockMode);
805     }
806 
807     /**
808      * Retrieves the key/data pair with the given key.  If the matching key has
809      * duplicate values, the first data item in the set of duplicates is
810      * returned. Retrieval of duplicates requires the use of {@link Cursor}
811      * operations.
812      *
813      * @param txn For a transactional database, an explicit transaction may be
814      * specified to transaction-protect the operation, or null may be specified
815      * to perform the operation without transaction protection.  For a
816      * non-transactional database, null must be specified.
817      *
818      * @param key the secondary key used as input.  It must be initialized with
819      * a non-null byte array by the caller.
820      *
821      * @param pKey the primary key returned as output.  Its byte array does not
822      * need to be initialized by the caller.
823      *
824      * @param data the primary data returned as output.  Its byte array does
825      * not need to be initialized by the caller.
826      *
827      * @param lockMode the locking attributes; if null, default attributes are
828      * used.
829      *
830      * @return {@link com.sleepycat.je.OperationStatus#NOTFOUND
831      * OperationStatus.NOTFOUND} if no matching key/data pair is found;
832      * otherwise, {@link com.sleepycat.je.OperationStatus#SUCCESS
833      * OperationStatus.SUCCESS}.
834      *
835      * @throws OperationFailureException if one of the <a
836      * href="OperationFailureException.html#readFailures">Read Operation
837      * Failures</a> occurs.
838      *
839      * @throws EnvironmentFailureException if an unexpected, internal or
840      * environment-wide failure occurs.
841      *
842      * @throws IllegalStateException if the database has been closed.
843      *
844      * @throws IllegalArgumentException if an invalid parameter is specified.
845      */
get(final Transaction txn, final DatabaseEntry key, final DatabaseEntry pKey, final DatabaseEntry data, LockMode lockMode)846     public OperationStatus get(final Transaction txn,
847                                final DatabaseEntry key,
848                                final DatabaseEntry pKey,
849                                final DatabaseEntry data,
850                                LockMode lockMode)
851         throws DatabaseException {
852 
853         checkEnv();
854         DatabaseUtil.checkForNullDbt(key, "key", true);
855         DatabaseUtil.checkForNullDbt(pKey, "pKey", false);
856         DatabaseUtil.checkForNullDbt(data, "data", false);
857         checkReadable("Can't call SecondaryDatabase.get:");
858         trace(Level.FINEST, "SecondaryDatabase.get", txn, key, null, lockMode);
859         if (getStat != null) {
860             getStat.increment();
861         }
862 
863         CursorConfig cursorConfig = CursorConfig.DEFAULT;
864         if (lockMode == LockMode.READ_COMMITTED) {
865             cursorConfig = CursorConfig.READ_COMMITTED;
866             lockMode = null;
867         }
868         checkLockModeWithoutTxn(txn, lockMode);
869 
870         Locker locker = null;
871         SecondaryCursor cursor = null;
872         OperationStatus commitStatus = null;
873         try {
874             locker = LockerFactory.getReadableLocker(
875                 this, txn, cursorConfig.getReadCommitted());
876             cursor = new SecondaryCursor(this, locker, cursorConfig);
877             commitStatus =
878                 cursor.search(key, pKey, data, lockMode, SearchMode.SET);
879             return commitStatus;
880         } catch (Error E) {
881             DbInternal.getEnvironmentImpl(envHandle).invalidate(E);
882             throw E;
883         } finally {
884             if (cursor != null) {
885                 cursor.close();
886             }
887 
888             if (locker != null) {
889                 locker.operationEnd(commitStatus);
890             }
891         }
892     }
893 
894     /**
895      * This operation is not allowed with this method signature. {@link
896      * UnsupportedOperationException} will always be thrown by this method.
897      * The corresponding method with the <code>pKey</code> parameter should be
898      * used instead.
899      */
900     @Override
getSearchBoth(final Transaction txn, final DatabaseEntry key, final DatabaseEntry data, final LockMode lockMode)901     public OperationStatus getSearchBoth(final Transaction txn,
902                                          final DatabaseEntry key,
903                                          final DatabaseEntry data,
904                                          final LockMode lockMode)
905         throws UnsupportedOperationException {
906 
907         throw notAllowedException();
908     }
909 
910     /**
911      * Retrieves the key/data pair with the specified secondary and primary
912      * key, that is, both the primary and secondary key items must match.
913      *
914      * @param txn For a transactional database, an explicit transaction may be
915      * specified to transaction-protect the operation, or null may be specified
916      * to perform the operation without transaction protection.  For a
917      * non-transactional database, null must be specified.
918      *
919      * @param key the secondary key used as input.  It must be initialized with
920      * a non-null byte array by the caller.
921      *
922      * @param pKey the primary key used as input.  It must be initialized with a
923      * non-null byte array by the caller.
924      *
925      * @param data the primary data returned as output.  Its byte array does not
926      * need to be initialized by the caller.
927      *
928      * @param lockMode the locking attributes; if null, default attributes are
929      * used.
930      *
931      * @return {@link com.sleepycat.je.OperationStatus#NOTFOUND
932      * OperationStatus.NOTFOUND} if no matching key/data pair is found;
933      * otherwise, {@link com.sleepycat.je.OperationStatus#SUCCESS
934      * OperationStatus.SUCCESS}.
935      *
936      * @throws OperationFailureException if one of the <a
937      * href="OperationFailureException.html#readFailures">Read Operation
938      * Failures</a> occurs.
939      *
940      * @throws EnvironmentFailureException if an unexpected, internal or
941      * environment-wide failure occurs.
942      *
943      * @throws IllegalStateException if the database has been closed.
944      *
945      * @throws IllegalArgumentException if an invalid parameter is specified.
946      */
getSearchBoth(final Transaction txn, final DatabaseEntry key, final DatabaseEntry pKey, final DatabaseEntry data, LockMode lockMode)947     public OperationStatus getSearchBoth(final Transaction txn,
948                                          final DatabaseEntry key,
949                                          final DatabaseEntry pKey,
950                                          final DatabaseEntry data,
951                                          LockMode lockMode)
952         throws DatabaseException {
953 
954         checkEnv();
955         DatabaseUtil.checkForNullDbt(key, "key", true);
956         DatabaseUtil.checkForNullDbt(pKey, "pKey", true);
957         DatabaseUtil.checkForNullDbt(data, "data", false);
958         checkReadable("Can't call SecondaryDatabase.getSearchBoth:");
959         trace(Level.FINEST, "SecondaryDatabase.getSearchBoth", txn, key, data,
960               lockMode);
961         if (getSearchBothStat != null) {
962             getSearchBothStat.increment();
963         }
964 
965         CursorConfig cursorConfig = CursorConfig.DEFAULT;
966         if (lockMode == LockMode.READ_COMMITTED) {
967             cursorConfig = CursorConfig.READ_COMMITTED;
968             lockMode = null;
969         }
970         checkLockModeWithoutTxn(txn, lockMode);
971 
972         Locker locker = null;
973         SecondaryCursor cursor = null;
974         OperationStatus commitStatus = null;
975         try {
976             locker = LockerFactory.getReadableLocker(
977                 this, txn, cursorConfig.getReadCommitted());
978             cursor = new SecondaryCursor(this, locker, cursorConfig);
979             commitStatus =
980                 cursor.search(key, pKey, data, lockMode, SearchMode.BOTH);
981             return commitStatus;
982         } catch (Error E) {
983             DbInternal.getEnvironmentImpl(envHandle).invalidate(E);
984             throw E;
985         } finally {
986             if (cursor != null) {
987                 cursor.close();
988             }
989 
990             if (locker != null) {
991                 locker.operationEnd(commitStatus);
992             }
993         }
994     }
995 
996     /**
997      * This operation is not allowed on a secondary database. {@link
998      * UnsupportedOperationException} will always be thrown by this method.
999      * The corresponding method on the primary database should be used instead.
1000      */
1001     @Override
put(final Transaction txn, final DatabaseEntry key, final DatabaseEntry data)1002     public OperationStatus put(final Transaction txn,
1003                                final DatabaseEntry key,
1004                                final DatabaseEntry data)
1005         throws UnsupportedOperationException {
1006 
1007         throw notAllowedException();
1008     }
1009 
1010     /**
1011      * This operation is not allowed on a secondary database. {@link
1012      * UnsupportedOperationException} will always be thrown by this method.
1013      * The corresponding method on the primary database should be used instead.
1014      */
1015     @Override
putNoOverwrite(final Transaction txn, final DatabaseEntry key, final DatabaseEntry data)1016     public OperationStatus putNoOverwrite(final Transaction txn,
1017                                           final DatabaseEntry key,
1018                                           final DatabaseEntry data)
1019         throws UnsupportedOperationException {
1020 
1021         throw notAllowedException();
1022     }
1023 
1024     /**
1025      * This operation is not allowed on a secondary database. {@link
1026      * UnsupportedOperationException} will always be thrown by this method.
1027      * The corresponding method on the primary database should be used instead.
1028      */
1029     @Override
putNoDupData(final Transaction txn, final DatabaseEntry key, final DatabaseEntry data)1030     public OperationStatus putNoDupData(final Transaction txn,
1031                                         final DatabaseEntry key,
1032                                         final DatabaseEntry data)
1033         throws UnsupportedOperationException {
1034 
1035         throw notAllowedException();
1036     }
1037 
1038     /**
1039      * This operation is not allowed on a secondary database. {@link
1040      * UnsupportedOperationException} will always be thrown by this method.
1041      * The corresponding method on the primary database should be used instead.
1042      */
1043     @Override
join(final Cursor[] cursors, final JoinConfig config)1044     public JoinCursor join(final Cursor[] cursors, final JoinConfig config)
1045         throws UnsupportedOperationException {
1046 
1047         throw notAllowedException();
1048     }
1049 
1050     /**
1051      * Updates a single secondary when a put() or delete() is performed on the
1052      * primary.
1053      * <p>
1054      * For an insert, newData will be non-null and oldData will be null.
1055      * <p>
1056      * For an update, newData will be non-null and oldData will be non-null.
1057      * <p>
1058      * For a delete, newData will be null and oldData may be null or non-null
1059      * depending on whether its need by the key creator/extractor.
1060      *
1061      * @param locker the internal locker.
1062      *
1063      * @param cursor secondary cursor to use, or null if this method should
1064      * open and close a cursor if one is needed.
1065      *
1066      * @param priKey the primary key.
1067      *
1068      * @param oldData the primary data before the change, or null if the record
1069      * did not previously exist.
1070      *
1071      * @param newData the primary data after the change, or null if the record
1072      * has been deleted.
1073      */
updateSecondary(final Locker locker, Cursor cursor, final DatabaseEntry priKey, final DatabaseEntry oldData, final DatabaseEntry newData)1074     void updateSecondary(final Locker locker,
1075                          Cursor cursor,
1076                          final DatabaseEntry priKey,
1077                          final DatabaseEntry oldData,
1078                          final DatabaseEntry newData)
1079         throws DatabaseException {
1080 
1081         SecondaryKeyCreator keyCreator = secondaryConfig.getKeyCreator();
1082         if (keyCreator != null) {
1083             /* Each primary record may have a single secondary key. */
1084             assert secondaryConfig.getMultiKeyCreator() == null;
1085 
1086             /* Get old and new secondary keys. */
1087             DatabaseEntry oldSecKey = null;
1088             if (oldData != null || newData == null) {
1089                 oldSecKey = new DatabaseEntry();
1090                 if (!keyCreator.createSecondaryKey(this, priKey, oldData,
1091                                                    oldSecKey)) {
1092                     oldSecKey = null;
1093                 }
1094             }
1095             DatabaseEntry newSecKey = null;
1096             if (newData != null) {
1097                 newSecKey = new DatabaseEntry();
1098                 if (!keyCreator.createSecondaryKey(this, priKey, newData,
1099                                                    newSecKey)) {
1100                     newSecKey = null;
1101                 }
1102             }
1103 
1104             /* Update secondary if old and new keys are unequal. */
1105             if ((oldSecKey != null && !oldSecKey.equals(newSecKey)) ||
1106                 (newSecKey != null && !newSecKey.equals(oldSecKey))) {
1107 
1108                 boolean localCursor = (cursor == null);
1109                 if (localCursor) {
1110                     cursor = new Cursor(this, locker, null);
1111                 }
1112                 try {
1113                     /* Delete the old key. */
1114                     if (oldSecKey != null) {
1115                         deleteKey(cursor, priKey, oldSecKey);
1116                     }
1117                     /* Insert the new key. */
1118                     if (newSecKey != null) {
1119                         insertKey(locker, cursor, priKey, newSecKey);
1120                     }
1121                 } finally {
1122                     if (localCursor && cursor != null) {
1123                         cursor.close();
1124                     }
1125                 }
1126             }
1127         } else {
1128             /* Each primary record may have multiple secondary keys. */
1129             SecondaryMultiKeyCreator multiKeyCreator =
1130                 secondaryConfig.getMultiKeyCreator();
1131             if (multiKeyCreator == null) {
1132                 throw new IllegalArgumentException(
1133                     "SecondaryConfig.getKeyCreator()/getMultiKeyCreator()" +
1134                     " may be null only if the primary database is read-only");
1135             }
1136 
1137             /* Get old and new secondary keys. */
1138             Set<DatabaseEntry> oldKeys = EMPTY_SET;
1139             Set<DatabaseEntry> newKeys = EMPTY_SET;
1140             if (oldData != null || newData == null) {
1141                 oldKeys = new HashSet<DatabaseEntry>();
1142                 multiKeyCreator.createSecondaryKeys(this, priKey,
1143                                                     oldData, oldKeys);
1144             }
1145             if (newData != null) {
1146                 newKeys = new HashSet<DatabaseEntry>();
1147                 multiKeyCreator.createSecondaryKeys(this, priKey,
1148                                                     newData, newKeys);
1149             }
1150 
1151             /* Update the secondary if there is a difference. */
1152             if (!oldKeys.equals(newKeys)) {
1153 
1154                 boolean localCursor = (cursor == null);
1155                 if (localCursor) {
1156                     cursor = new Cursor(this, locker, null);
1157                 }
1158                 try {
1159                     /* Delete old keys that are no longer present. */
1160                     Set<DatabaseEntry> oldKeysCopy = oldKeys;
1161                     if (oldKeys != EMPTY_SET) {
1162                         oldKeysCopy = new HashSet<DatabaseEntry>(oldKeys);
1163                         oldKeys.removeAll(newKeys);
1164                         for (Iterator<DatabaseEntry> i = oldKeys.iterator();
1165                              i.hasNext();) {
1166                             DatabaseEntry oldKey = i.next();
1167                             deleteKey(cursor, priKey, oldKey);
1168                         }
1169                     }
1170 
1171                     /* Insert new keys that were not present before. */
1172                     if (newKeys != EMPTY_SET) {
1173                         newKeys.removeAll(oldKeysCopy);
1174                         for (Iterator<DatabaseEntry> i = newKeys.iterator();
1175                              i.hasNext();) {
1176                             DatabaseEntry newKey = i.next();
1177                             insertKey(locker, cursor, priKey, newKey);
1178                         }
1179                     }
1180                 } finally {
1181                     if (localCursor && cursor != null) {
1182                         cursor.close();
1183                     }
1184                 }
1185             }
1186         }
1187     }
1188 
1189     /**
1190      * Deletes an old secondary key.
1191      */
deleteKey(final Cursor cursor, final DatabaseEntry priKey, final DatabaseEntry oldSecKey)1192     private void deleteKey(final Cursor cursor,
1193                            final DatabaseEntry priKey,
1194                            final DatabaseEntry oldSecKey)
1195         throws DatabaseException {
1196 
1197         OperationStatus status =
1198             cursor.search(oldSecKey, priKey,
1199                           LockMode.RMW,
1200                           SearchMode.BOTH);
1201         if (status == OperationStatus.SUCCESS) {
1202             cursor.deleteInternal(getDatabaseImpl().getRepContext());
1203             return;
1204         }
1205         if (isFullyPopulated) {
1206             throw new SecondaryIntegrityException(
1207                 cursor.getCursorImpl().getLocker(),
1208                 "Secondary is corrupt: the primary record contains a key " +
1209                 "that is not present in the secondary",
1210                 getDebugName(), oldSecKey, priKey);
1211         }
1212     }
1213 
1214     /**
1215      * Inserts a new secondary key.
1216      */
insertKey(final Locker locker, final Cursor cursor, final DatabaseEntry priKey, final DatabaseEntry newSecKey)1217     private void insertKey(final Locker locker,
1218                            final Cursor cursor,
1219                            final DatabaseEntry priKey,
1220                            final DatabaseEntry newSecKey)
1221         throws DatabaseException {
1222 
1223         /* Check for the existence of a foreign key. */
1224         Database foreignDb =
1225             secondaryConfig.getForeignKeyDatabase();
1226         if (foreignDb != null) {
1227             Cursor foreignCursor = null;
1228             try {
1229                 foreignCursor = new Cursor(foreignDb, locker,
1230                                            null);
1231                 DatabaseEntry tmpData = new DatabaseEntry();
1232                 OperationStatus status =
1233                     foreignCursor.search(newSecKey, tmpData,
1234                                          LockMode.DEFAULT,
1235                                          SearchMode.SET);
1236                 if (status != OperationStatus.SUCCESS) {
1237                     throw new ForeignConstraintException(
1238                         locker,
1239                         "Secondary " + getDebugName() +
1240                         " foreign key not allowed: it is not" +
1241                         " present in the foreign database " +
1242                         foreignDb.getDebugName(), getDebugName(),
1243                         newSecKey, priKey);
1244                 }
1245             } finally {
1246                 if (foreignCursor != null) {
1247                     foreignCursor.close();
1248                 }
1249             }
1250         }
1251 
1252         /* Insert the new key. */
1253         if (configuration.getSortedDuplicates()) {
1254             OperationStatus status = cursor.putInternal(newSecKey, priKey,
1255                                                         PutMode.NO_DUP_DATA);
1256             if (status != OperationStatus.SUCCESS && isFullyPopulated) {
1257                 throw new SecondaryIntegrityException(
1258                     locker, "Secondary/primary record already present",
1259                     getDebugName(), newSecKey, priKey);
1260             }
1261         } else {
1262             OperationStatus status = cursor.putInternal(newSecKey, priKey,
1263                                                         PutMode.NO_OVERWRITE);
1264             if (status != OperationStatus.SUCCESS && isFullyPopulated) {
1265                 throw new UniqueConstraintException(
1266                     locker, "Unique secondary key is already present",
1267                     getDebugName(), newSecKey, priKey);
1268             }
1269         }
1270     }
1271 
1272     /**
1273      * Called when a record in the foreign database is deleted.
1274      *
1275      * @param secKey is the primary key of the foreign database, which is the
1276      * secondary key (ordinary key) of this secondary database.
1277      */
onForeignKeyDelete(final Locker locker, final DatabaseEntry secKey)1278     void onForeignKeyDelete(final Locker locker, final DatabaseEntry secKey)
1279         throws DatabaseException {
1280 
1281         final ForeignKeyDeleteAction deleteAction =
1282             secondaryConfig.getForeignKeyDeleteAction();
1283 
1284         /* Use RMW if we're going to be deleting the secondary records. */
1285         final LockMode lockMode =
1286             (deleteAction == ForeignKeyDeleteAction.ABORT) ?
1287             LockMode.DEFAULT :
1288             LockMode.RMW;
1289 
1290         /*
1291          * Use the deleted foreign primary key to read the data of this
1292          * database, which is the associated primary's key.
1293          */
1294         final Cursor cursor = new Cursor(this, locker, null);
1295         try {
1296             final DatabaseEntry priKey = new DatabaseEntry();
1297             OperationStatus status =
1298                 cursor.search(secKey, priKey, lockMode, SearchMode.SET);
1299             while (status == OperationStatus.SUCCESS) {
1300 
1301                 if (deleteAction == ForeignKeyDeleteAction.ABORT) {
1302 
1303                     /*
1304                      * ABORT - throw an exception to cause the user to abort
1305                      * the transaction.
1306                      */
1307                     throw new DeleteConstraintException(
1308                         locker, "Secondary refers to a deleted foreign key",
1309                         getDebugName(), secKey, priKey);
1310 
1311                 } else if (deleteAction == ForeignKeyDeleteAction.CASCADE) {
1312 
1313                     /*
1314                      * CASCADE - delete the associated primary record.
1315                      */
1316                     final Database primaryDb = getPrimary(priKey);
1317                     if (primaryDb != null) {
1318                         status = primaryDb.deleteInternal(locker, priKey);
1319                         if (status != OperationStatus.SUCCESS) {
1320                             throw secondaryRefersToMissingPrimaryKey(
1321                                 locker, secKey, priKey);
1322                         }
1323                     }
1324 
1325                 } else if (deleteAction == ForeignKeyDeleteAction.NULLIFY) {
1326 
1327                     /*
1328                      * NULLIFY - set the secondary key to null in the
1329                      * associated primary record.
1330                      */
1331                     final Database primaryDb = getPrimary(priKey);
1332                     if (primaryDb != null) {
1333                         final Cursor priCursor =
1334                             new Cursor(primaryDb, locker, null);
1335                         try {
1336                             final DatabaseEntry data = new DatabaseEntry();
1337                             status = priCursor.search(
1338                                 priKey, data, LockMode.RMW, SearchMode.SET);
1339                             if (status != OperationStatus.SUCCESS) {
1340                                 throw secondaryRefersToMissingPrimaryKey(
1341                                     locker, secKey, priKey);
1342                             }
1343                             final ForeignMultiKeyNullifier multiNullifier =
1344                                 secondaryConfig.getForeignMultiKeyNullifier();
1345                             if (multiNullifier != null) {
1346                                 if (multiNullifier.nullifyForeignKey(
1347                                         this, priKey, data, secKey)) {
1348                                     priCursor.putCurrent(data);
1349                                 }
1350                             } else {
1351                                 final ForeignKeyNullifier nullifier =
1352                                     secondaryConfig.getForeignKeyNullifier();
1353                                 if (nullifier.nullifyForeignKey(this, data)) {
1354                                     priCursor.putCurrent(data);
1355                                 }
1356                             }
1357                         } finally {
1358                             priCursor.close();
1359                         }
1360                     }
1361                 } else {
1362                     /* Should never occur. */
1363                     throw EnvironmentFailureException.unexpectedState();
1364                 }
1365 
1366                 status = cursor.retrieveNext(secKey, priKey, LockMode.DEFAULT,
1367                                              GetMode.NEXT_DUP);
1368             }
1369         } finally {
1370             cursor.close();
1371         }
1372     }
1373 
1374     /**
1375      * If either ImmutableSecondaryKey or ExtractFromPrimaryKeyOnly is
1376      * configured, an update cannot change a secondary key.
1377      * ImmutableSecondaryKey is a guarantee from the user meaning just that,
1378      * and ExtractFromPrimaryKeyOnly also implies the secondary key cannot
1379      * change because it is derived from the primary key which is immutable
1380      * (like any other key).
1381      */
updateMayChangeSecondary()1382     boolean updateMayChangeSecondary() {
1383         return !secondaryConfig.getImmutableSecondaryKey() &&
1384                !secondaryConfig.getExtractFromPrimaryKeyOnly();
1385     }
1386 
1387     /**
1388      * When false is returned, this allows optimizing for the case where a
1389      * primary update operation can update secondaries without reading the
1390      * primary data.
1391      */
1392     static boolean
needOldDataForUpdate(final Collection<SecondaryDatabase> secondaries)1393         needOldDataForUpdate(final Collection<SecondaryDatabase> secondaries) {
1394         if (secondaries == null) {
1395             return false;
1396         }
1397         for (final SecondaryDatabase secDb : secondaries) {
1398             if (secDb.updateMayChangeSecondary()) {
1399                 return true;
1400             }
1401         }
1402         return false;
1403     }
1404 
1405     /**
1406      * When false is returned, this allows optimizing for the case where a
1407      * primary delete operation can update secondaries without reading the
1408      * primary data.
1409      */
1410     static boolean
needOldDataForDelete(final Collection<SecondaryDatabase> secondaries)1411         needOldDataForDelete(final Collection<SecondaryDatabase> secondaries) {
1412         if (secondaries == null) {
1413             return false;
1414         }
1415         for (final SecondaryDatabase secDb : secondaries) {
1416             if (!secDb.secondaryConfig.getExtractFromPrimaryKeyOnly()) {
1417                 return true;
1418             }
1419         }
1420         return false;
1421     }
1422 
1423     /* A secondary DB has no secondaries of its own, by definition. */
1424     @Override
hasSecondaryOrForeignKeyAssociations()1425     boolean hasSecondaryOrForeignKeyAssociations() {
1426         return false;
1427     }
1428 
1429     /**
1430      * Utility to call SecondaryAssociation.getPrimary.
1431      *
1432      * Handles exceptions and does an important debugging check that can't be
1433      * done at database open time: ensures that the same SecondaryAssociation
1434      * instance is used for all associated DBs.
1435      * <p>
1436      * Returns null if getPrimary returns null, so the caller must handle this
1437      * possibility.  Null normally means that a secondary read operation can
1438      * skip the record.
1439      */
getPrimary(DatabaseEntry priKey)1440     Database getPrimary(DatabaseEntry priKey) {
1441         final Database priDb;
1442         try {
1443             priDb = secAssoc.getPrimary(priKey);
1444         } catch (RuntimeException e) {
1445             throw EnvironmentFailureException.unexpectedException(
1446                 "Exception from SecondaryAssociation.getPrimary", e);
1447         }
1448         if (priDb == null) {
1449             return null;
1450         }
1451         if (priDb.secAssoc != secAssoc) {
1452             throw new IllegalArgumentException(
1453                 "Primary and secondary have different SecondaryAssociation " +
1454                 "instances. Remember to configure the SecondaryAssociation " +
1455                 "on the primary database.");
1456         }
1457         return priDb;
1458     }
1459 
checkReadable(final String msg)1460     private void checkReadable(final String msg) {
1461         checkOpen(msg);
1462         if (!isFullyPopulated) {
1463             throw new IllegalStateException(
1464                 msg + " Incremental population is currently enabled.");
1465         }
1466     }
1467 
setupThroughputStats(EnvironmentImpl envImpl)1468     private void setupThroughputStats(EnvironmentImpl envImpl) {
1469         getStat = envImpl.getThroughputStat(THROUGHPUT_SECONDARYDB_GET);
1470         deleteStat = envImpl.getThroughputStat(THROUGHPUT_SECONDARYDB_DELETE);
1471         getSearchBothStat =
1472             envImpl.getThroughputStat(THROUGHPUT_SECONDARYDB_GETSEARCHBOTH);
1473     }
1474 
notAllowedException()1475     static UnsupportedOperationException notAllowedException() {
1476 
1477         return new UnsupportedOperationException(
1478             "Operation not allowed on a secondary");
1479     }
1480 
1481     /**
1482      * Send trace messages to the java.util.logger. Don't rely on the logger
1483      * alone to conditionalize whether we send this message, we don't even want
1484      * to construct the message if the level is not enabled.
1485      */
trace(final Level level, final String methodName)1486     void trace(final Level level, final String methodName) {
1487         if (logger.isLoggable(level)) {
1488             StringBuilder sb = new StringBuilder();
1489             sb.append(methodName);
1490             sb.append(" name=").append(getDebugName());
1491             sb.append(" primary=").append(primaryDatabase.getDebugName());
1492 
1493             LoggerUtils.logMsg(
1494                 logger, envHandle.getEnvironmentImpl(), level, sb.toString());
1495         }
1496     }
1497 }
1498