Package com.sleepycat.je

Source Code of com.sleepycat.je.SecondaryDatabase

/*-
* See the file LICENSE for redistribution information.
*
* Copyright (c) 2002, 2011 Oracle and/or its affiliates.  All rights reserved.
*
*/

package com.sleepycat.je;

import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Level;

import com.sleepycat.je.dbi.DatabaseImpl;
import com.sleepycat.je.dbi.GetMode;
import com.sleepycat.je.dbi.PutMode;
import com.sleepycat.je.dbi.CursorImpl.SearchMode;
import com.sleepycat.je.txn.Locker;
import com.sleepycat.je.txn.LockerFactory;
import com.sleepycat.je.utilint.DatabaseUtil;
import com.sleepycat.je.utilint.LoggerUtils;

/**
* A secondary database handle.
*
* <p>Secondary databases are opened with {@link
* Environment#openSecondaryDatabase Environment.openSecondaryDatabase} and are
* always associated with a single primary database.  The distinguishing
* characteristics of a secondary database are:</p>
*
* <ul> <li>Records are automatically added to a secondary database when
* records are added, modified and deleted in the primary database.  Direct
* calls to <code>put()</code> methods on a secondary database are
* prohibited.</li>
* <li>The {@link #delete delete} method of a secondary database will delete
* the primary record and as well as all its associated secondary records.</li>
* <li>Calls to all <code>get()</code> methods will return the data from the
* associated primary database.</li>
* <li>Additional <code>get()</code> method signatures are provided to return
* the primary key in an additional <code>pKey</code> parameter.</li>
* <li>Calls to {@link #openCursor openCursor} will return a {@link
* SecondaryCursor}, which itself has <code>get()</code> methods that return
* the data of the primary database and additional <code>get()</code> method
* signatures for returning the primary key.</li>
* </ul>
* <p>Before opening or creating a secondary database you must implement
* the {@link SecondaryKeyCreator} or {@link SecondaryMultiKeyCreator}
* interface.</p>
*
* <p>For example, to create a secondary database that supports duplicates:</p>
*
* <pre>
*     Database primaryDb; // The primary database must already be open.
*     SecondaryKeyCreator keyCreator; // Your key creator implementation.
*     SecondaryConfig secConfig = new SecondaryConfig();
*     secConfig.setAllowCreate(true);
*     secConfig.setSortedDuplicates(true);
*     secConfig.setKeyCreator(keyCreator);
*     SecondaryDatabase newDb = env.openSecondaryDatabase(transaction,
*                                                         "myDatabaseName",
*                                                         primaryDb,
*                                                         secConfig)
* </pre>
*
* <p>If a primary database is to be associated with one or more secondary
* databases, it may not be configured for duplicates.</p>
*
* <p><b>WARNING:</b> The associations between primary and secondary databases
* are not stored persistently.  Whenever a primary database is opened for
* write access by the application, the appropriate associated secondary
* databases should also be opened by the application.  This is necessary to
* ensure data integrity when changes are made to the primary database.  If the
* secondary database is not opened, it will not be updated when the primary is
* updated, and the references between the databases will become invalid.
* (Note that this warning does not apply when using the {@link
* com.sleepycat.persist DPL}, which does store secondary relationships
* persistently.)</p>
*
* <h3><a name="transactions">Special considerations for using Secondary
* Databases with and without Transactions</a></h3>
*
* <p>Normally, during a primary database write operation (insert, update or
* delete), all associated secondary databases are also updated.  However, when
* an exception occurs during the write operation, the updates may be
* incomplete.  If the databases are transactional, this is handled by aborting
* the transaction to undo the incomplete operation.  If an auto-commit
* transaction is used (null is passed for the transaction), the transaction
* will be aborted automatically.  If an explicit transaction is used, it
* must be aborted by the application caller after the exception is caught.</p>
*
* <p>However, if the databases are non-transactional, integrity problems can
* result when an exception occurs during the write operation.  Because the
* write operation is not made atomic by a transaction, references between the
* databases will become invalid if the operation is incomplete.  This results
* in a {@link SecondaryIntegrityException} when attempting to access the
* databases later.</p>
*
* <p>A secondary integrity problem is persistent; it cannot be resolved by
* reopening the databases or the environment.  The only way to resolve the
* problem is to restore the environment from a valid backup, or, if the
* integrity of the primary database is assumed, to remove and recreate all
* secondary databases.</p>
*
* <p>Therefore, secondary databases and indexes should always be used in
* conjunction with transactional databases and stores. Without transactions,
* it is the responsibility of the application to handle the results of the
* incomplete write operation or to take steps to prevent this situation from
* happening in the first place.</p>
*
* <p>The following exceptions may be thrown during a write operation, and may
* cause an integrity problem in the absence of transactions.</p>
* <ul>
* <li>{@link SecondaryConstraintException}, see its subclasses for more
* information.</li>
* <li>{@link LockConflictException}, when more than one thread is accessing
* the databases.</li>
* <li>{@link EnvironmentFailureException}, if an unexpected or system failure
* occurs.</li>
* <li>There is always the possibility of an {@link Error} or an unintended
* {@link RuntimeException}.</li>
* </ul>
*/
public class SecondaryDatabase extends Database {

    /* For type-safe check against EMPTY_SET */
    private static final Set<DatabaseEntry> EMPTY_SET =
        Collections.emptySet();

    private Database primaryDb;
    private SecondaryConfig secondaryConfig;
    private SecondaryTrigger secondaryTrigger;
    private ForeignKeyTrigger foreignKeyTrigger;

    /**
     * Creates a secondary database but does not open or fully initialize it.
     *
     * @throws IllegalArgumentException via Environment.openSecondaryDatabase.
     */
    SecondaryDatabase(final Environment env,
                      final SecondaryConfig secConfig,
                      final Database primaryDatabase)
        throws DatabaseException {

        super(env);
        DatabaseUtil.checkForNullParam(primaryDatabase, "primaryDatabase");
        primaryDatabase.checkOpen("Can't use as primary:");
        if (primaryDatabase.configuration.getSortedDuplicates()) {
            throw new IllegalArgumentException
                ("Duplicates must not be allowed for a primary database: " +
                 primaryDatabase.getDebugName());
        }
        if (env.getEnvironmentImpl() !=
                primaryDatabase.getEnvironment().getEnvironmentImpl()) {
            throw new IllegalArgumentException
                ("Primary and secondary databases must be in the same" +
                 " environment");
        }
        if (secConfig.getKeyCreator() != null &&
            secConfig.getMultiKeyCreator() != null) {
            throw new IllegalArgumentException
                ("secConfig.getKeyCreator() and getMultiKeyCreator() may not" +
                 " both be non-null");
        }
        if (!primaryDatabase.configuration.getReadOnly() &&
            secConfig.getKeyCreator() == null &&
            secConfig.getMultiKeyCreator() == null) {
            throw new IllegalArgumentException
                ("secConfig and getKeyCreator()/getMultiKeyCreator()" +
                 " may be null only if the primary database is read-only");
        }
        if (secConfig.getForeignKeyNullifier() != null &&
            secConfig.getForeignMultiKeyNullifier() != null) {
            throw new IllegalArgumentException
                ("secConfig.getForeignKeyNullifier() and" +
                 " getForeignMultiKeyNullifier() may not both be non-null");
        }
        if (secConfig.getForeignKeyDeleteAction() ==
                         ForeignKeyDeleteAction.NULLIFY &&
            secConfig.getForeignKeyNullifier() == null &&
            secConfig.getForeignMultiKeyNullifier() == null) {
            throw new IllegalArgumentException
                ("ForeignKeyNullifier or ForeignMultiKeyNullifier must be" +
                 " non-null when ForeignKeyDeleteAction is NULLIFY");
        }
        if (secConfig.getForeignKeyNullifier() != null &&
            secConfig.getMultiKeyCreator() != null) {
            throw new IllegalArgumentException
                ("ForeignKeyNullifier may not be used with" +
                 " SecondaryMultiKeyCreator -- use" +
                 " ForeignMultiKeyNullifier instead");
        }
        if (secConfig.getForeignKeyDatabase() != null) {
            Database foreignDb = secConfig.getForeignKeyDatabase();
            if (foreignDb.getDatabaseImpl().getSortedDuplicates()) {
                throw new IllegalArgumentException
                    ("Duplicates must not be allowed for a foreign key " +
                     " database: " + foreignDb.getDebugName());
            }
        }
        primaryDb = primaryDatabase;
        secondaryTrigger = new SecondaryTrigger(this);
        if (secConfig.getForeignKeyDatabase() != null) {
            foreignKeyTrigger = new ForeignKeyTrigger(this);
        }
    }

    /**
     * Create a database, called by Environment
     */
    @Override
    DatabaseImpl initNew(final Environment env,
                         final Locker locker,
                         final String databaseName,
                         final DatabaseConfig dbConfig)
        throws DatabaseException {

        final DatabaseImpl dbImpl =
            super.initNew(env, locker, databaseName, dbConfig);
        init(locker);
        return dbImpl;
    }

    /**
     * Open a database, called by Environment
     *
     * @throws IllegalArgumentException via Environment.openSecondaryDatabase.
     */
    @Override
    void initExisting(final Environment env,
                      final Locker locker,
                      final DatabaseImpl database,
                      final DatabaseConfig dbConfig)
        throws DatabaseException {

        /* Disallow one secondary associated with two different primaries. */
        Database otherPriDb = database.findPrimaryDatabase();
        if (otherPriDb != null &&
            otherPriDb.getDatabaseImpl() != primaryDb.getDatabaseImpl()) {
            throw new IllegalArgumentException
                ("Secondary is already associated with a different primary: " +
                 otherPriDb.getDebugName());
        }

        super.initExisting(env, locker, database, dbConfig);
        init(locker);
    }

    /**
     * Adds secondary to primary's list, and populates the secondary if needed.
     *
     * @param locker should be the locker used to open the database.  If a
     * transactional locker, the population operations will occur in the same
     * transaction; this may result in a large number of retained locks.  If a
     * non-transactional locker, the Cursor will create a ThreadLocker (even if
     * a BasicLocker used for handle locking is passed), and locks will not be
     * retained.
     */
    private void init(final Locker locker)
        throws DatabaseException {

        trace(Level.FINEST, "SecondaryDatabase open");

        secondaryConfig = (SecondaryConfig) configuration;

        /*
         * Insert foreign key triggers at the front of the list and append
         * secondary triggers at the end, so that ForeignKeyDeleteAction.ABORT
         * is applied before deleting the secondary keys.
         */
        primaryDb.addTrigger(secondaryTrigger, false);

        Database foreignDb = secondaryConfig.getForeignKeyDatabase();
        if (foreignDb != null) {
            foreignDb.addTrigger(foreignKeyTrigger, true);
        }

        /* Populate secondary if requested and secondary is empty. */
        if (secondaryConfig.getAllowPopulate()) {
            Cursor secCursor = null;
            Cursor priCursor = null;
            try {
                secCursor = new Cursor(this, locker, null);
                DatabaseEntry key = new DatabaseEntry();
                DatabaseEntry data = new DatabaseEntry();
                OperationStatus status = secCursor.position(key, data,
                                                            LockMode.DEFAULT,
                                                            true);
                if (status == OperationStatus.NOTFOUND) {
                    /* Is empty, so populate */
                    priCursor = new Cursor(primaryDb, locker, null);
                    status = priCursor.position(key, data, LockMode.DEFAULT,
                                                true);
                    while (status == OperationStatus.SUCCESS) {
                        updateSecondary(locker, secCursor, key, null, data);
                        status = priCursor.retrieveNext(key, data,
                                                        LockMode.DEFAULT,
                                                        GetMode.NEXT);
                    }
                }
            } finally {
                if (secCursor != null) {
                    secCursor.close();
                }
                if (priCursor != null) {
                    priCursor.close();
                }
            }
        }
    }

    /**
     * Closes a secondary database and dis-associates it from its primary
     * database. A secondary database should be closed before closing its
     * associated primary database.
     *
     * {@inheritDoc}
     *
     * <!-- inherit other javadoc from overridden method -->
     */
    @Override
    public synchronized void close()
        throws DatabaseException {

        if (primaryDb != null && secondaryTrigger != null) {
            primaryDb.removeTrigger(secondaryTrigger);
        }
        Database foreignDb = secondaryConfig.getForeignKeyDatabase();
        if (foreignDb != null && foreignKeyTrigger != null) {
            foreignDb.removeTrigger(foreignKeyTrigger);
        }
        super.close();
    }

    /**
     * Should be called by the secondaryTrigger while holding a write lock on
     * the trigger list.
     */
    void clearPrimary() {
        primaryDb = null;
        secondaryTrigger  = null;
    }

    /**
     * Should be called by the foreignKeyTrigger while holding a write lock on
     * the trigger list.
     */
    void clearForeignKeyTrigger() {
        foreignKeyTrigger = null;
    }

    /**
     * Returns the primary database associated with this secondary database.
     *
     * @return the primary database associated with this secondary database.
     */
    public Database getPrimaryDatabase() {
        return primaryDb;
    }

    /**
     * Returns a copy of the secondary configuration of this database.
     *
     * @return a copy of the secondary configuration of this database.
     *
     * @throws EnvironmentFailureException if an unexpected, internal or
     * environment-wide failure occurs.
     *
     * @deprecated As of JE 4.0.13, replaced by {@link
     * SecondaryDatabase#getConfig()}.</p>
     */
    public SecondaryConfig getSecondaryConfig()
        throws DatabaseException {

        return (SecondaryConfig) getConfig();
    }

    /**
     * Returns a copy of the secondary configuration of this database.
     *
     * @return a copy of the secondary configuration of this database.
     *
     * @throws EnvironmentFailureException if an unexpected, internal or
     * environment-wide failure occurs.
     */
    public SecondaryConfig getConfig()
        throws DatabaseException {

        return (SecondaryConfig) super.getConfig();
    }

    /**
     * Returns the secondary config without cloning, for internal use.
     */
    public SecondaryConfig getPrivateSecondaryConfig() {
        return secondaryConfig;
    }

    /**
     * Obtain a cursor on a database, returning a
     * <code>SecondaryCursor</code>. Calling this method is the equivalent of
     * calling {@link #openCursor} and casting the result to {@link
     * SecondaryCursor}.
     *
     * @param txn the transaction used to protect all operations performed with
     * the cursor, or null if the operations should not be transaction
     * protected.  If the database is non-transactional, null must be
     * specified.  For a transactional database, the transaction is optional
     * for read-only access and required for read-write access.
     *
     * @param cursorConfig The cursor attributes.  If null, default attributes
     * are used.
     *
     * @return A secondary database cursor.
     *
     * @throws EnvironmentFailureException if an unexpected, internal or
     * environment-wide failure occurs.
     *
     * @deprecated As of JE 4.0.13, replaced by {@link
     * SecondaryDatabase#openCursor}.</p>
     */
    public SecondaryCursor openSecondaryCursor(final Transaction txn,
                                               final CursorConfig cursorConfig)
        throws DatabaseException {

        return (SecondaryCursor) openCursor(txn, cursorConfig);
    }

    /**
     * Obtain a cursor on a database, returning a <code>SecondaryCursor</code>.
     */
    @Override
    public SecondaryCursor openCursor(final Transaction txn,
                                      final CursorConfig cursorConfig)
        throws DatabaseException {

        return (SecondaryCursor) super.openCursor(txn, cursorConfig);
    }

    /**
     * Overrides Database method.
     */
    @Override
    Cursor newDbcInstance(final Transaction txn,
                          final CursorConfig cursorConfig)
        throws DatabaseException {

        return new SecondaryCursor(this, txn, cursorConfig);
    }

    /**
     * Deletes the primary key/data pair associated with the specified
     * secondary key.  In the presence of duplicate key values, all primary
     * records associated with the designated secondary key will be deleted.
     *
     * When the primary records are deleted, their associated secondary records
     * are deleted as if {@link Database#delete} were called.  This includes,
     * but is not limited to, the secondary record referenced by the given key.
     *
     * @param key the secondary key used as input.  It must be initialized with
     * a non-null byte array by the caller.
     *
     * <!-- inherit other javadoc from overridden method -->
     */
    @Override
    public OperationStatus delete(final Transaction txn,
                                  final DatabaseEntry key)
        throws DeleteConstraintException,
               LockConflictException,
               DatabaseException,
               UnsupportedOperationException,
               IllegalArgumentException {

        checkEnv();
        DatabaseUtil.checkForNullDbt(key, "key", true);
        checkOpen("Can't call SecondaryDatabase.delete:");
        trace(Level.FINEST, "SecondaryDatabase.delete", txn,
              key, null, null);

        Locker locker = null;
        Cursor cursor = null;

        OperationStatus commitStatus = OperationStatus.NOTFOUND;
        try {
            locker = LockerFactory.getWritableLocker
                (envHandle,
                 txn,
                 getDatabaseImpl().isInternalDb(),
                 isTransactional(),
                 getDatabaseImpl().isReplicated()); // autoTxnIsReplicated

            /* Read the primary key (the data of a secondary). */
            cursor = new Cursor(this, locker, null);
            DatabaseEntry pKey = new DatabaseEntry();
            OperationStatus searchStatus =
                cursor.search(key, pKey, LockMode.RMW, SearchMode.SET);

            /*
             * For each duplicate secondary key, delete the primary record and
             * all its associated secondary records, including the one
             * referenced by this secondary cursor.
             */
            while (searchStatus == OperationStatus.SUCCESS) {
                commitStatus = primaryDb.deleteInternal(locker, pKey, null);
                if (commitStatus != OperationStatus.SUCCESS) {
                    throw secondaryRefersToMissingPrimaryKey
                        (locker, key, pKey);
                }
                searchStatus = cursor.retrieveNext
                    (key, pKey, LockMode.RMW, GetMode.NEXT_DUP);
            }
            return commitStatus;
        } catch (Error E) {
            DbInternal.getEnvironmentImpl(envHandle).invalidate(E);
            throw E;
        } finally {
            if (cursor != null) {
                cursor.close();
            }
            if (locker != null) {
                locker.operationEnd(commitStatus);
            }
        }
    }

    /**
     * @param key the secondary key used as input.  It must be initialized with
     * a non-null byte array by the caller.
     *
     * @param data the primary data returned as output.  Its byte array does
     * not need to be initialized by the caller.
     *
     * <!-- inherit other javadoc from overridden method -->
     */
    @Override
    public OperationStatus get(final Transaction txn,
                               final DatabaseEntry key,
                               final DatabaseEntry data,
                               final LockMode lockMode)
        throws DatabaseException {

        return get(txn, key, new DatabaseEntry(), data, lockMode);
    }

    /**
     * Retrieves the key/data pair with the given key.  If the matching key has
     * duplicate values, the first data item in the set of duplicates is
     * returned. Retrieval of duplicates requires the use of {@link Cursor}
     * operations.
     *
     * @param txn For a transactional database, an explicit transaction may be
     * specified to transaction-protect the operation, or null may be specified
     * to perform the operation without transaction protection.  For a
     * non-transactional database, null must be specified.
     *
     * @param key the secondary key used as input.  It must be initialized with
     * a non-null byte array by the caller.
     *
     * @param pKey the primary key returned as output.  Its byte array does not
     * need to be initialized by the caller.
     *
     * @param data the primary data returned as output.  Its byte array does
     * not need to be initialized by the caller.
     *
     * @param lockMode the locking attributes; if null, default attributes are
     * used.
     *
     * @return {@link com.sleepycat.je.OperationStatus#NOTFOUND
     * OperationStatus.NOTFOUND} if no matching key/data pair is found;
     * otherwise, {@link com.sleepycat.je.OperationStatus#SUCCESS
     * OperationStatus.SUCCESS}.
     *
     * @throws OperationFailureException if one of the <a
     * href="OperationFailureException.html#readFailures">Read Operation
     * Failures</a> occurs.
     *
     * @throws EnvironmentFailureException if an unexpected, internal or
     * environment-wide failure occurs.
     *
     * @throws IllegalStateException if the database has been closed.
     *
     * @throws IllegalArgumentException if an invalid parameter is specified.
     */
    public OperationStatus get(final Transaction txn,
                               final DatabaseEntry key,
                               final DatabaseEntry pKey,
                               final DatabaseEntry data,
                               LockMode lockMode)
        throws DatabaseException {

        checkEnv();
        DatabaseUtil.checkForNullDbt(key, "key", true);
        DatabaseUtil.checkForNullDbt(pKey, "pKey", false);
        DatabaseUtil.checkForNullDbt(data, "data", false);
        checkOpen("Can't call SecondaryDatabase.get:");
        trace(Level.FINEST, "SecondaryDatabase.get", txn, key, null, lockMode);

        CursorConfig cursorConfig = CursorConfig.DEFAULT;
        if (lockMode == LockMode.READ_COMMITTED) {
            cursorConfig = CursorConfig.READ_COMMITTED;
            lockMode = null;
        }
        checkLockModeWithoutTxn(txn, lockMode);

        Locker locker = null;
        SecondaryCursor cursor = null;
        OperationStatus commitStatus = null;
        try {
            locker = LockerFactory.getReadableLocker
                (envHandle, txn, isTransactional(),
                 cursorConfig.getReadCommitted());
            cursor = new SecondaryCursor(this, locker, cursorConfig);
            commitStatus =
                cursor.search(key, pKey, data, lockMode, SearchMode.SET);
            return commitStatus;
        } catch (Error E) {
            DbInternal.getEnvironmentImpl(envHandle).invalidate(E);
            throw E;
        } finally {
            if (cursor != null) {
                cursor.close();
            }

            if (locker != null) {
                locker.operationEnd(commitStatus);
            }
        }
    }

    /**
     * This operation is not allowed with this method signature. {@link
     * UnsupportedOperationException} will always be thrown by this method.
     * The corresponding method with the <code>pKey</code> parameter should be
     * used instead.
     */
    @Override
    public OperationStatus getSearchBoth(final Transaction txn,
                                         final DatabaseEntry key,
                                         final DatabaseEntry data,
                                         final LockMode lockMode)
        throws UnsupportedOperationException {

        throw notAllowedException();
    }

    /**
     * Retrieves the key/data pair with the specified secondary and primary
     * key, that is, both the primary and secondary key items must match.
     *
     * @param txn For a transactional database, an explicit transaction may be
     * specified to transaction-protect the operation, or null may be specified
     * to perform the operation without transaction protection.  For a
     * non-transactional database, null must be specified.
     *
     * @param key the secondary key used as input.  It must be initialized with
     * a non-null byte array by the caller.
     *
     * @param pKey the primary key used as input.  It must be initialized with a
     * non-null byte array by the caller.
     *
     * @param data the primary data returned as output.  Its byte array does not
     * need to be initialized by the caller.
     *
     * @param lockMode the locking attributes; if null, default attributes are
     * used.
     *
     * @return {@link com.sleepycat.je.OperationStatus#NOTFOUND
     * OperationStatus.NOTFOUND} if no matching key/data pair is found;
     * otherwise, {@link com.sleepycat.je.OperationStatus#SUCCESS
     * OperationStatus.SUCCESS}.
     *
     * @throws OperationFailureException if one of the <a
     * href="OperationFailureException.html#readFailures">Read Operation
     * Failures</a> occurs.
     *
     * @throws EnvironmentFailureException if an unexpected, internal or
     * environment-wide failure occurs.
     *
     * @throws IllegalStateException if the database has been closed.
     *
     * @throws IllegalArgumentException if an invalid parameter is specified.
     */
    public OperationStatus getSearchBoth(final Transaction txn,
                                         final DatabaseEntry key,
                                         final DatabaseEntry pKey,
                                         final DatabaseEntry data,
                                         LockMode lockMode)
        throws DatabaseException {

        checkEnv();
        DatabaseUtil.checkForNullDbt(key, "key", true);
        DatabaseUtil.checkForNullDbt(pKey, "pKey", true);
        DatabaseUtil.checkForNullDbt(data, "data", false);
        checkOpen("Can't call SecondaryDatabase.getSearchBoth:");
        trace(Level.FINEST, "SecondaryDatabase.getSearchBoth", txn, key, data,
              lockMode);

        CursorConfig cursorConfig = CursorConfig.DEFAULT;
        if (lockMode == LockMode.READ_COMMITTED) {
            cursorConfig = CursorConfig.READ_COMMITTED;
            lockMode = null;
        }
        checkLockModeWithoutTxn(txn, lockMode);

        Locker locker = null;
        SecondaryCursor cursor = null;
        OperationStatus commitStatus = null;
        try {
            locker = LockerFactory.getReadableLocker
                (envHandle, txn, isTransactional(),
                 cursorConfig.getReadCommitted());
            cursor = new SecondaryCursor(this, locker, cursorConfig);
            commitStatus =
                cursor.search(key, pKey, data, lockMode, SearchMode.BOTH);
            return commitStatus;
        } catch (Error E) {
            DbInternal.getEnvironmentImpl(envHandle).invalidate(E);
            throw E;
        } finally {
            if (cursor != null) {
                cursor.close();
            }

            if (locker != null) {
                locker.operationEnd(commitStatus);
            }
        }
    }

    /**
     * This operation is not allowed on a secondary database. {@link
     * UnsupportedOperationException} will always be thrown by this method.
     * The corresponding method on the primary database should be used instead.
     */
    @Override
    public OperationStatus put(final Transaction txn,
                               final DatabaseEntry key,
                               final DatabaseEntry data)
        throws UnsupportedOperationException {

        throw notAllowedException();
    }

    /**
     * This operation is not allowed on a secondary database. {@link
     * UnsupportedOperationException} will always be thrown by this method.
     * The corresponding method on the primary database should be used instead.
     */
    @Override
    public OperationStatus putNoOverwrite(final Transaction txn,
                                          final DatabaseEntry key,
                                          final DatabaseEntry data)
        throws UnsupportedOperationException {

        throw notAllowedException();
    }

    /**
     * This operation is not allowed on a secondary database. {@link
     * UnsupportedOperationException} will always be thrown by this method.
     * The corresponding method on the primary database should be used instead.
     */
    @Override
    public OperationStatus putNoDupData(final Transaction txn,
                                        final DatabaseEntry key,
                                        final DatabaseEntry data)
        throws UnsupportedOperationException {

        throw notAllowedException();
    }

    /**
     * This operation is not allowed on a secondary database. {@link
     * UnsupportedOperationException} will always be thrown by this method.
     * The corresponding method on the primary database should be used instead.
     */
    @Override
    public JoinCursor join(final Cursor[] cursors, final JoinConfig config)
        throws UnsupportedOperationException {

        throw notAllowedException();
    }

    /**
     * Updates a single secondary when a put() or delete() is performed on the
     * primary.
     *
     * @param locker the internal locker.
     *
     * @param cursor secondary cursor to use, or null if this method should
     * open and close a cursor if one is needed.
     *
     * @param priKey the primary key.
     *
     * @param oldData the primary data before the change, or null if the record
     * did not previously exist.
     *
     * @param newData the primary data after the change, or null if the record
     * has been deleted.
     */
    void updateSecondary(final Locker locker,
                         Cursor cursor,
                         final DatabaseEntry priKey,
                         final DatabaseEntry oldData,
                         final DatabaseEntry newData)
        throws DatabaseException {

        /*
         * If we're updating the primary and the secondary key cannot be
         * changed, optimize for that case by doing nothing.
         */
        if (secondaryConfig.getImmutableSecondaryKey() &&
            oldData != null && newData != null) {
            return;
        }

        SecondaryKeyCreator keyCreator = secondaryConfig.getKeyCreator();
        if (keyCreator != null) {
            /* Each primary record may have a single secondary key. */
            assert secondaryConfig.getMultiKeyCreator() == null;

            /* Get old and new secondary keys. */
            DatabaseEntry oldSecKey = null;
            if (oldData != null) {
                oldSecKey = new DatabaseEntry();
                if (!keyCreator.createSecondaryKey(this, priKey, oldData,
                                                   oldSecKey)) {
                    oldSecKey = null;
                }
            }
            DatabaseEntry newSecKey = null;
            if (newData != null) {
                newSecKey = new DatabaseEntry();
                if (!keyCreator.createSecondaryKey(this, priKey, newData,
                                                   newSecKey)) {
                    newSecKey = null;
                }
            }

            /* Update secondary if old and new keys are unequal. */
            if ((oldSecKey != null && !oldSecKey.equals(newSecKey)) ||
                (newSecKey != null && !newSecKey.equals(oldSecKey))) {

                boolean localCursor = (cursor == null);
                if (localCursor) {
                    cursor = new Cursor(this, locker, null);
                }
                try {
                    /* Delete the old key. */
                    if (oldSecKey != null) {
                        deleteKey(cursor, priKey, oldSecKey);
                    }
                    /* Insert the new key. */
                    if (newSecKey != null) {
                        insertKey(locker, cursor, priKey, newSecKey);
                    }
                } finally {
                    if (localCursor && cursor != null) {
                        cursor.close();
                    }
                }
            }
        } else {
            /* Each primary record may have multiple secondary keys. */
            SecondaryMultiKeyCreator multiKeyCreator =
                secondaryConfig.getMultiKeyCreator();
            assert multiKeyCreator != null;

            /* Get old and new secondary keys. */
            Set<DatabaseEntry> oldKeys = EMPTY_SET;
            Set<DatabaseEntry> newKeys = EMPTY_SET;
            if (oldData != null) {
                oldKeys = new HashSet<DatabaseEntry>();
                multiKeyCreator.createSecondaryKeys(this, priKey,
                                                    oldData, oldKeys);
            }
            if (newData != null) {
                newKeys = new HashSet<DatabaseEntry>();
                multiKeyCreator.createSecondaryKeys(this, priKey,
                                                    newData, newKeys);
            }

            /* Update the secondary if there is a difference. */
            if (!oldKeys.equals(newKeys)) {

                boolean localCursor = (cursor == null);
                if (localCursor) {
                    cursor = new Cursor(this, locker, null);
                }
                try {
                    /* Delete old keys that are no longer present. */
                    Set<DatabaseEntry> oldKeysCopy = oldKeys;
                    if (oldKeys != EMPTY_SET) {
                        oldKeysCopy = new HashSet<DatabaseEntry>(oldKeys);
                        oldKeys.removeAll(newKeys);
                        for (Iterator<DatabaseEntry> i = oldKeys.iterator();
                             i.hasNext();) {
                            DatabaseEntry oldKey = i.next();
                            deleteKey(cursor, priKey, oldKey);
                        }
                    }

                    /* Insert new keys that were not present before. */
                    if (newKeys != EMPTY_SET) {
                        newKeys.removeAll(oldKeysCopy);
                        for (Iterator<DatabaseEntry> i = newKeys.iterator();
                             i.hasNext();) {
                            DatabaseEntry newKey = i.next();
                            insertKey(locker, cursor, priKey, newKey);
                        }
                    }
                } finally {
                    if (localCursor && cursor != null) {
                        cursor.close();
                    }
                }
            }
        }
    }

    /**
     * Deletes an old secondary key.
     */
    private void deleteKey(final Cursor cursor,
                           final DatabaseEntry priKey,
                           final DatabaseEntry oldSecKey)
        throws DatabaseException {

        OperationStatus status =
            cursor.search(oldSecKey, priKey,
                          LockMode.RMW,
                          SearchMode.BOTH);
        if (status == OperationStatus.SUCCESS) {
            cursor.deleteInternal(getDatabaseImpl().getRepContext());
        } else {
            throw new SecondaryIntegrityException
                (cursor.getCursorImpl().getLocker(),
                 "Secondary is corrupt: the primary record contains a key " +
                 "that is not present in the secondary",
                getDebugName(), oldSecKey, priKey);
        }
    }

    /**
     * Inserts a new secondary key.
     */
    private void insertKey(final Locker locker,
                           final Cursor cursor,
                           final DatabaseEntry priKey,
                           final DatabaseEntry newSecKey)
        throws DatabaseException {

        /* Check for the existence of a foreign key. */
        Database foreignDb =
            secondaryConfig.getForeignKeyDatabase();
        if (foreignDb != null) {
            Cursor foreignCursor = null;
            try {
                foreignCursor = new Cursor(foreignDb, locker,
                                           null);
                DatabaseEntry tmpData = new DatabaseEntry();
                OperationStatus status =
                    foreignCursor.search(newSecKey, tmpData,
                                         LockMode.DEFAULT,
                                         SearchMode.SET);
                if (status != OperationStatus.SUCCESS) {
                    throw new ForeignConstraintException
                        (locker,
                         "Secondary " + getDebugName() +
                         " foreign key not allowed: it is not" +
                         " present in the foreign database " +
                         foreignDb.getDebugName(), getDebugName(),
                         newSecKey, priKey);
                }
            } finally {
                if (foreignCursor != null) {
                    foreignCursor.close();
                }
            }
        }

        /* Insert the new key. */
        if (configuration.getSortedDuplicates()) {
            OperationStatus status = cursor.putInternal(newSecKey, priKey,
                                                        PutMode.NO_DUP_DATA);
            if (status != OperationStatus.SUCCESS) {
                throw new SecondaryIntegrityException
                    (locker, "Secondary/primary record already present",
                     getDebugName(), newSecKey, priKey);
            }
        } else {
            OperationStatus status = cursor.putInternal(newSecKey, priKey,
                                                        PutMode.NO_OVERWRITE);
            if (status != OperationStatus.SUCCESS) {
                throw new UniqueConstraintException
                    (locker, "Unique secondary key is already present",
                     getDebugName(), newSecKey, priKey);
            }
        }
    }

    /**
     * Called by the ForeignKeyTrigger when a record in the foreign database is
     * deleted.
     *
     * @param secKey is the primary key of the foreign database, which is the
     * secondary key (ordinary key) of this secondary database.
     */
    void onForeignKeyDelete(final Locker locker, final DatabaseEntry secKey)
        throws DatabaseException {

        ForeignKeyDeleteAction deleteAction =
            secondaryConfig.getForeignKeyDeleteAction();

        /* Use RMW if we're going to be deleting the secondary records. */
        LockMode lockMode = (deleteAction == ForeignKeyDeleteAction.ABORT) ?
            LockMode.DEFAULT :
            LockMode.RMW;

        /*
         * Use the deleted foreign primary key to read the data of this
         * database, which is the associated primary's key.
         */
        DatabaseEntry priKey = new DatabaseEntry();
        Cursor cursor = null;
        OperationStatus status;
        try {
            cursor = new Cursor(this, locker, null);
            status = cursor.search(secKey, priKey, lockMode,
                                   SearchMode.SET);
            while (status == OperationStatus.SUCCESS) {

                if (deleteAction == ForeignKeyDeleteAction.ABORT) {

                    /*
                     * ABORT - throw an exception to cause the user to abort
                     * the transaction.
                     */
                    throw new DeleteConstraintException
                        (locker, "Secondary refers to a deleted foreign key",
                         getDebugName(), secKey, priKey);

                } else if (deleteAction == ForeignKeyDeleteAction.CASCADE) {

                    /*
                     * CASCADE - delete the associated primary record.
                     */
                    Cursor priCursor = null;
                    try {
                        DatabaseEntry data = new DatabaseEntry();
                        priCursor = new Cursor(primaryDb, locker, null);
                        status = priCursor.search(priKey, data, LockMode.RMW,
                                                  SearchMode.SET);
                        if (status == OperationStatus.SUCCESS) {
                            priCursor.delete();
                        } else {
                            throw secondaryRefersToMissingPrimaryKey
                                (locker, secKey, priKey);
                        }
                    } finally {
                        if (priCursor != null) {
                            priCursor.close();
                        }
                    }

                } else if (deleteAction == ForeignKeyDeleteAction.NULLIFY) {

                    /*
                     * NULLIFY - set the secondary key to null in the
                     * associated primary record.
                     */
                    Cursor priCursor = null;
                    try {
                        DatabaseEntry data = new DatabaseEntry();
                        priCursor = new Cursor(primaryDb, locker, null);
                        status = priCursor.search(priKey, data, LockMode.RMW,
                                                  SearchMode.SET);
                        if (status == OperationStatus.SUCCESS) {
                            ForeignMultiKeyNullifier multiNullifier =
                                secondaryConfig.getForeignMultiKeyNullifier();
                            if (multiNullifier != null) {
                                if (multiNullifier.nullifyForeignKey
                                        (this, priKey, data, secKey)) {
                                    priCursor.putCurrent(data);
                                }
                            } else {
                                ForeignKeyNullifier nullifier =
                                    secondaryConfig.getForeignKeyNullifier();
                                if (nullifier.nullifyForeignKey
                                        (this, data)) {
                                    priCursor.putCurrent(data);
                                }
                            }
                        } else {
                            throw secondaryRefersToMissingPrimaryKey
                                (locker, secKey, priKey);
                        }
                    } finally {
                        if (priCursor != null) {
                            priCursor.close();
                        }
                    }
                } else {
                    /* Should never occur. */
                    throw EnvironmentFailureException.unexpectedState();
                }

                status = cursor.retrieveNext(secKey, priKey, LockMode.DEFAULT,
                                             GetMode.NEXT_DUP);
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    SecondaryIntegrityException
        secondaryRefersToMissingPrimaryKey(final Locker locker,
                                           final DatabaseEntry secKey,
                                           final DatabaseEntry priKey)
        throws DatabaseException {

        return new SecondaryIntegrityException
            (locker,
             "Secondary refers to a missing key in the primary database",
             getDebugName(), secKey, priKey);
    }

    static UnsupportedOperationException notAllowedException() {

        return new UnsupportedOperationException
            ("Operation not allowed on a secondary");
    }

    /**
     * Send trace messages to the java.util.logger. Don't rely on the logger
     * alone to conditionalize whether we send this message, we don't even want
     * to construct the message if the level is not enabled.
     */
    void trace(final Level level, final String methodName) {
        if (logger.isLoggable(level)) {
            StringBuilder sb = new StringBuilder();
            sb.append(methodName);
            sb.append(" name=").append(getDebugName());
            sb.append(" primary=").append(primaryDb.getDebugName());

            LoggerUtils.logMsg
                (logger, envHandle.getEnvironmentImpl(), level, sb.toString());
        }
    }
}
TOP

Related Classes of com.sleepycat.je.SecondaryDatabase

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.