All Downloads are FREE. Search and download functionalities are using the official Maven repository.

oracle.kv.impl.api.ops.Scanner Maven / Gradle / Ivy

Go to download

NoSQL Database Server - supplies build and runtime support for the server (store) side of the Oracle NoSQL Database.

The newest version!
/*-
 * Copyright (C) 2011, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This file was distributed by Oracle as part of a version of Oracle NoSQL
 * Database made available at:
 *
 * http://www.oracle.com/technetwork/database/database-technologies/nosqldb/downloads/index.html
 *
 * Please see the LICENSE file included in the top-level directory of the
 * appropriate version of Oracle NoSQL Database for a copy of the license and
 * additional information.
 */

package oracle.kv.impl.api.ops;

import static oracle.kv.impl.api.ops.InternalOperationHandler.MIN_READ;
import static oracle.kv.impl.api.ops.InternalOperationHandler.getStorageSize;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Set;

import oracle.kv.Depth;
import oracle.kv.Direction;
import oracle.kv.Key;
import oracle.kv.KeyRange;
import oracle.kv.impl.api.table.TableImpl;
import oracle.kv.impl.rep.RepNode;
import oracle.kv.impl.topo.PartitionId;
import oracle.kv.impl.util.TxnUtil;

import com.sleepycat.je.Cursor;
import com.sleepycat.je.CursorConfig;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseNotFoundException;
import com.sleepycat.je.DbInternal;
import com.sleepycat.je.DbInternal.Search;
import com.sleepycat.je.Environment;
import com.sleepycat.je.Get;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationResult;
import com.sleepycat.je.ScanFilter;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.dbi.RangeConstraint;

/**
 * A class to encapsulate iteration over JE records for KV methods that
 * require iteration.
 *
 * The usage pattern is:
 *   Scanner scanner = new Scanner(...);
 *   DatabaseEntry keyEntry = scanner.getKey();
 *   DatabaseEntry dateEntry = scanner.getDate();
 *   try {
 *     while (scanner.next()) {
 *        // do things with keyEntry, dataEntry
 *     }
 *   } finally {
 *     scanner.close();  // closes cursor
 *   }
 *
 * Any security-relevant operations are performed by callers. Batching is
 * also handled by callers.
 *
 * See below for more information on state.
 */

public class Scanner {

    /** Lowest possible value for a serialized key character. */
    private static final byte MIN_VALUE_BYTE = ((byte) 1);
    /** Lowest possible value for a serialized key character. */
    private static final char MIN_VALUE_CHAR = ((char) 1);

    /**
     * The max value for a serialized byte is 0xFE and the min value is
     * 0xFF (it sorts before zero). See comments at the top of Key.java.
     */
    private static final int MIN_KEY_BYTE = 0xFF;
    private static final int MAX_KEY_BYTE = 0xFE;
    
    /** Minimum possible key. */
    private static final byte[] MIN_KEY = new byte[0];

    /** Same key comparator as used for KV keys */
    static final Comparator KEY_BYTES_COMPARATOR =
        new Key.BytesComparator();

    private final InternalOperation op;
    private final byte[] parentKey;
    private final boolean majorPathComplete;
    private final KeyRange subRange;
    private final Direction direction;
    private final byte[] resumeKey;

    /*
     * If false, resume will start at the resume key rather than moving to
     * the 1st key after the resume key. It may be false only in the case of
     * a query containing a NESTED TABLES clause, or if a SizeLimitException
     * is thrown during query processing.
     */
    private final boolean moveAfterResumeKey;

    private final CursorConfig cursorConfig;
    private final LockMode lockMode;
    private final boolean includeParent;
    private final boolean allDescendants;
    private final Database db;
    private final Cursor cursor;
    private final int nChildComponents;

    /*
     * These could be passed in by callers if that makes more sense. It'd avoid
     * the need for getters to access the keys and data.
     */
    private final DatabaseEntry keyEntry;
    private final DatabaseEntry dataEntry;

    /*
     * Members that are relatively static, but will change during the first
     * couple of calls to next().
     *
     * The State is used by next() to handle the transitions from a
     * not-yet-started scan to looking for the parent, to the steady-state
     * iteration loop. These states are:
     * INIT -- no calls to next() have been made. When called this state the
     * Scanner is initialized and the parent search is done if necessary.
     * FIRST_DESC -- this state indicates that the parent search has been done
     * and the iteration is ready to find the first descendant (either forward
     * or reverse)
     * ALL -- this is the steady state for the iteration where most calls are
     * JE getNext()/getPrev().
     */
    private enum State { INIT, FIRST_DESC, ALL }
    private State state;

    private boolean parentFound;
    private byte[] keyPrefix;
    private byte[] searchInitKey;
    private RangeConstraint rangeConstraint;
    private OperationResult result;

    /* Set to false if disable charging cost for reading key, default is true */
    private boolean chargeKeyRead;

    /*
     * Construction.
     *
     * @param txn is the current transaction. It is used to create the cursor.
     *
     * @param partitionId the id of the partition being iterated. This
     * corresponds to a JE database.
     *
     * @param repNode the current RepNode; used to get the database.
     *
     * @param parentKey is the byte array of the parent Key parameter, and may
     * be null for store iteration.
     *
     * @param majorPathComplete is true if the parentKey's major path is
     * complete and therefore the subRange applies to the next minor component,
     * or false if the major path is incomplete (there is no minor path) and
     * therefore the subRange applies to the next major component.  Must be
     * true for multiGet iteration and false for store iteration.
     *
     * @param subRange used for ranged iteration. May be null.
     *
     * @param the depth to use for iteration. For tables this is always
     * Depth.PARENT_AND_CHILDREN.
     *
     * @param direction is the direction of the scan. This must always be
     * Direction.FORWARD or Direction.REVERSE.
     *
     * @param resumeKey is the key after which to resume the iteration of
     * descendants, or null to start at the parent.  To resume the iteration
     * where it left off, pass the last key returned.
     *
     * @param cursorConfig the CursorConfig to use for the scan.  Should be
     * DEFAULT for a non-iterator operation and READ_COMMITTED for an iterator
     * operation.
     *
     * @param lockMode the LockMode to use for the iteration.
     *
     * A Scanner instance must be closed using Scanner.close() or the cursor
     * that is created on construction will remain open.
     */
    public Scanner(InternalOperation op,
                   Transaction txn,
                   PartitionId partitionId,
                   RepNode repNode,
                   byte[] parentKey,
                   boolean majorPathComplete,
                   KeyRange subRange,
                   Depth depth,
                   Direction direction,
                   byte[] resumeKey,
                   boolean moveAfterResumeKey,
                   CursorConfig cursorConfig,
                   LockMode lockMode,
                   boolean keyOnly) {

        this.op = op;
        this.parentKey = parentKey;
        this.majorPathComplete = majorPathComplete;
        this.direction = direction;
        this.resumeKey = resumeKey;
        this.moveAfterResumeKey = moveAfterResumeKey;
        this.cursorConfig = cursorConfig;
        this.lockMode = lockMode;

        if (subRange != null &&
            subRange.isPrefix() &&
            subRange.getStart().length() == 0) {
            this.subRange = null;
        } else {
            this.subRange = subRange;
        }

        includeParent =
            (resumeKey == null) &&
            ((depth == Depth.PARENT_AND_CHILDREN) ||
             (depth == Depth.PARENT_AND_DESCENDANTS));

        allDescendants =
            (depth == Depth.DESCENDANTS_ONLY) ||
            (depth == Depth.PARENT_AND_DESCENDANTS);

        assert (direction == Direction.FORWARD ||
                direction == Direction.REVERSE);

        db = repNode.getPartitionDB(partitionId);

        keyEntry = new DatabaseEntry();
        dataEntry = new DatabaseEntry();
        if (keyOnly) {
            dataEntry.setPartial(0, 0, true);
        }

        cursor = db.openCursor(txn, cursorConfig);

        state = State.INIT;
        parentFound = false;
        nChildComponents = (parentKey != null) ?
            (Key.countComponents(parentKey) + 1) : 1;

        /* Charge the cost of reading key */
        chargeKeyRead = true;
    }

    /**
     * Returns the key
     */
    public final DatabaseEntry getKey() {
        return keyEntry;
    }

    /**
     * Returns the data
     */
    public final DatabaseEntry getData() {
        return dataEntry;
    }

    /**
     * Returns the cursor
     */
    public final Cursor getCursor() {
        return cursor;
    }

    public final LockMode getLockMode() {
        return lockMode;
    }

    /**
     * This is called to lock the current data entry for key-only scans.
     *
     * This method is called after getNext() method, because the data entry is
     * skipped to read in this call, and the read cost of reading key was
     * recorded in getNext() method, so no read cost is needed to record here.
     */
    public boolean getCurrent() {
        assert dataEntry.getPartial();
        result = cursor.get(keyEntry, dataEntry,
                            Get.CURRENT, LockMode.DEFAULT.toReadOptions());
        return (result != null);
    }

    /**
     * This is called if the dataEntry is partial and the data needs to be
     * locked and fetched.
     *
     */
    public boolean getLockedData(DatabaseEntry newEntry) {
        assert !newEntry.getPartial();
        result = cursor.get(keyEntry, newEntry,
                            Get.CURRENT, LockMode.DEFAULT.toReadOptions());
        return processResult(result, newEntry);
    }

    /**
     * Returns the JE database used for the Scanner.
     */
    public Database getDatabase() {
        return db;
    }

    /**
     * Returns the expiration time of the current valid result if non-null,
     * otherwise 0.
     *
     * This means that the caller must have received a true result from
     * one of the navigational interfaces indicating there's a current
     * record.
     */
    public long getExpirationTime() {
        return (result != null ? result.getExpirationTime() : 0);
    }

    /**
     * Returns the current OperationResult. If the most recent operation failed
     * to find a record this will be null.
     */
    public OperationResult getResult() {
        return result;
    }

    /**
     * Closes the scanner.
     */
    public void close() {
        TxnUtil.close(cursor);
    }

    /**
     * Returns true if there is a "next" record, regardless of direction.
     * The key and data for the record are in the key and data members.
     */
    public boolean next() {

        if (direction == Direction.FORWARD) {
            return getNext();
        }
        return getPrev();
    }

    public int getCurrentStorageSize() {
        return getStorageSize(cursor);
    }

    /*
     * Forward iteration
     */

    private boolean getNext() {

        if (state == State.INIT) {
            initForward();
            /* Do separate search for parent. */
            if (includeParent && parentKey != null) {

                keyEntry.setData(parentKey);
                result = cursor.get(keyEntry, dataEntry,
                                    Get.SEARCH, lockMode.toReadOptions());
                /*
                 * Don't charge the empty read if parent key is not found, since
                 * continue to read its first descendant.
                 */
                parentFound = processResult(result, dataEntry,
                                            true /*noChargeEmpty*/);
            }
            if (parentFound) {
                return true;
            }
        }

        if (state == State.FIRST_DESC) {
            if (!getFirstDescendant()) {
                return false;
            }

            /*
             * In this case we are at the first descendant now.
             */
            if (allDescendants) {
                return true;
            }
            return handleImmediateChildren();
        }

        assert(state == State.ALL);
        result = cursor.get(keyEntry, dataEntry,
                            Get.NEXT, lockMode.toReadOptions());
        processResult(result, dataEntry);
        if (result == null) {
            return false;
        }

        /* Simple case: collect all descendants. */
        if (allDescendants) {
            return true;
        }

        return handleImmediateChildren();
    }

    /*
     * Determine the search and scan constraints.  The searchInitKey is
     * used to start the scan for descendents, and is used when we do not
     * first position on the parent. The rangeConstraint is used to end the
     * scan and prevents scanning outside the parent key descendents (or
     * sub-range of child keys), which is particularly important to avoid
     * deadlocks with other transactions.
     */
    private void initForward() {
        state = State.FIRST_DESC;
        /*
         * Determine the search and scan constraints.  The searchInitKey is
         * used to start the scan for descendents, and is used when we do not
         * first position on the parent. The rangeConstraint is used to end the
         * scan and prevents scanning outside the parent key descendents (or
         * sub-range of child keys), which is particularly important to avoid
         * deadlocks with other transactions.
         */
        if (subRange == null) {

            /*
             * Case 1: No KeyRange.  Start the scan at first descendant and
             * stop it at a key that doesn't start with the parent key prefix.
             * Scan entire key set when parentKey is null.
             */
            if (parentKey != null) {
                searchInitKey =
                    Key.addComponent(parentKey, majorPathComplete, "");
                rangeConstraint = getPrefixConstraint(searchInitKey);
            } else {
                searchInitKey = MIN_KEY;
                rangeConstraint = null;
            }

        } else if (subRange.isPrefix()) {

            /*
             * Case 2: KeyRange is a prefix.  Start the scan at the first child
             * key with the given KeyRange prefix and stop it at a key that
             * doesn't start with that prefix.
             */
            searchInitKey = Key.addComponent
                (parentKey, majorPathComplete, subRange.getStart());
            rangeConstraint = getPrefixConstraint(searchInitKey);

        } else {

            /*
             * Case 3: KeyRange has different start/end points.  Start the scan
             * at the child key with the inclusive KeyRange start key.
             */
            if (subRange.getStart() != null) {

                final String rangeStart = subRange.getStartInclusive() ?
                    subRange.getStart() :
                    getPathComponentSuccessor(subRange.getStart());

                searchInitKey = Key.addComponent
                    (parentKey, majorPathComplete, rangeStart);

            } else if (parentKey != null) {
                searchInitKey = Key.addComponent(parentKey, majorPathComplete,
                                                 "");
            } else {
                searchInitKey = MIN_KEY;
            }

            /* Stop the scan at the exclusive KeyRange end key. */
            if (subRange.getEnd() != null) {

                final String rangeEnd = subRange.getEndInclusive() ?
                    getPathComponentSuccessor(subRange.getEnd()) :
                    subRange.getEnd();

                rangeConstraint = getRangeEndConstraint
                    (Key.addComponent(parentKey, majorPathComplete, rangeEnd));

            } else if (parentKey != null) {
                rangeConstraint = getPrefixConstraint
                    (Key.addComponent(parentKey, majorPathComplete, ""));
            } else {
                rangeConstraint = null;
            }
        }
        assert (searchInitKey != null);
    }

    private boolean getFirstDescendant() {
        state = State.ALL;
        /*
         * For a non-prefix range there is a possibility that the range
         * start (or MIN_KEY) exceeds the range end. The rangeConstraint
         * won't detect this if the searchInitKey (range start or MIN_KEY)
         * matches a record exactly.
         */
        if ((subRange != null) &&
            (rangeConstraint != null) &&
            !subRange.isPrefix() &&
            !rangeConstraint.inBounds(searchInitKey)) {
            result = null;
            return false;
        }

        /* Move to first descendant. */
        cursor.setRangeConstraint(rangeConstraint);
        if (parentFound && subRange == null) {
            result = cursor.get(keyEntry, dataEntry,
                                Get.NEXT, lockMode.toReadOptions());
            processResult(result, dataEntry);
        } else if (resumeKey != null) {
            keyEntry.setData(resumeKey);
            result = cursor.get(keyEntry, dataEntry,
                                Get.SEARCH_GTE, lockMode.toReadOptions());
            if (result != null &&
                moveAfterResumeKey &&
                Arrays.equals(resumeKey, keyEntry.getData())) {
                result = cursor.get(keyEntry, dataEntry,
                                    Get.NEXT, lockMode.toReadOptions());
                processResult(result, dataEntry);
            } else {
                /*
                 * Only record the read cost if resume key is not found. If
                 * found, continue to read the next of resume key.
                 */
                processResult(result, dataEntry);
            }
        } else {
            keyEntry.setData(searchInitKey);
            result = cursor.get(keyEntry, dataEntry,
                                Get.SEARCH_GTE, lockMode.toReadOptions());
            processResult(result, dataEntry);
        }
        return (result != null);
    }

    private boolean handleImmediateChildren() {

        /*
         * To collect immediate children only, we position at the child's
         * successor key when we encounter a descendant that is not an
         * immediate child.
         */
        while (result != null) {
            final int nComponents =
                Key.countComponents(keyEntry.getData());
            if (nComponents == nChildComponents) {
                /* This is an immediate child. */
                return true;
            }
            /* Not an immediate child.  Move to child's successor. */
            assert nComponents > nChildComponents;
            getChildKeySuccessor(keyEntry, parentKey);
            result = cursor.get(keyEntry, dataEntry,
                                Get.SEARCH_GTE, lockMode.toReadOptions());
            processResult(result, dataEntry);
        }
        return false;
    }


    /*
     * Reverse iteration
     */

    /*
     * The keyPrefix is used both to create a rangeConstraint and to find the
     * first record following the range as the starting point. The
     * searchInitKey is only used as a starting point when a KeyRange end point
     * is given. The rangeConstraint is used to end the scan.
     */
    private void initReverse() {

        state = State.FIRST_DESC;

        /*
         * For a reverse scan, to avoid deadlocks with transactions that move
         * the cursor forward we must use read-committed (which is always used
         * for an iteration and in this method) and a non-sticky cursor.
         * Read-committed ensures that we release locks as we move the cursor.
         * The non-sticky cursor ensures that we release the lock at the
         * current position before getting the lock at the next position, as
         * opposed to overlapping the locks as occurs with a default, sticky
         * cursor.
         */
        assert cursorConfig.getReadCommitted();

        if (subRange == null) {

            /*
             * Case 1: No KeyRange.
             */
            if (parentKey != null) {

                /*
                 * Use the parent key prefix as the range constraint and
                 * starting point.
                 */
                keyPrefix = Key.addComponent(parentKey, majorPathComplete, "");
                searchInitKey = null;
                rangeConstraint = getPrefixConstraint(keyPrefix);
            } else {
                /* Scan all keys, no constraints. */
                keyPrefix = null;
                searchInitKey = null;
                rangeConstraint = null;
            }

        } else if (subRange.isPrefix()) {

            /*
             * Case 2: KeyRange is a prefix.  Same as case 1 but using the
             * KeyRange prefix appended to the parent key.
             */
            keyPrefix = Key.addComponent(parentKey, majorPathComplete,
                                         subRange.getStart());
            searchInitKey = null;
            rangeConstraint = getPrefixConstraint(keyPrefix);

        } else {

            /*
             * Case 3: KeyRange has different start/end points.  The key prefix
             * may be used in several different ways below.
             */
            if (parentKey != null) {
                keyPrefix = Key.addComponent(parentKey, majorPathComplete, "");
            } else {
                keyPrefix = null;
            }

            if (subRange.getEnd() != null) {

                /*
                 * Start the scan at the child key with the exclusive KeyRange
                 * end key.
                 */
                final String rangeEnd = subRange.getEndInclusive() ?
                    getPathComponentSuccessor(subRange.getEnd()) :
                    subRange.getEnd();

                searchInitKey = Key.addComponent
                    (parentKey, majorPathComplete, rangeEnd);
            } else {
                /* Use the keyPrefix, if any, to start the scan. */
                searchInitKey = null;
            }

            if (subRange.getStart() != null) {

                /*
                 * Stop the scan at the inclusive KeyRange start key.
                 */
                final String rangeStart = subRange.getStartInclusive() ?
                    subRange.getStart() :
                    getPathComponentSuccessor(subRange.getStart());

                rangeConstraint = getRangeStartConstraint(
                    Key.addComponent(
                        parentKey, majorPathComplete, rangeStart));

            } else if (keyPrefix != null) {
                /* Use the keyPrefix to stop the scan. */
                rangeConstraint = getPrefixConstraint(keyPrefix);
            } else {
                /* Scan all keys, no constraints. */
                rangeConstraint = null;
            }
        }
    }

    private boolean getPrev() {

        if (state == State.INIT) {
            initReverse();

            /* Do separate search for parent. */
            state = State.FIRST_DESC;
            if (includeParent && parentKey != null) {
                keyEntry.setData(parentKey);
                result = cursor.get(keyEntry, dataEntry,
                                    Get.SEARCH, lockMode.toReadOptions());
                /*
                 * Don't charge the empty read if parent key is not found, since
                 * continue to read its first descendant.
                 */
                processResult(result, dataEntry, true /* noChargeEmpty */);
                if (result != null) {
                    return true;
                }
            }
        }

        if (state == State.FIRST_DESC) {
            state = State.ALL;
            /*
             * Move to the desired key using an exclusive end point. Do not set
             * the rangeConstraint at first, because we may temporarily move
             * outside of the range during the DbInternal.search call.
             */
            final byte[] exclusiveInitKey;
            Search search = Search.LT;

            if (resumeKey != null) {
                exclusiveInitKey = resumeKey;
                if (!moveAfterResumeKey) {
                    search = Search.LTE;
                }
            } else if (searchInitKey != null) {
                exclusiveInitKey = searchInitKey;
            } else if (keyPrefix != null) {
                final byte[] nextPrefix = getPrefixSuccessor(keyPrefix);
                exclusiveInitKey = (nextPrefix != null) ? nextPrefix : null;
            } else {
                exclusiveInitKey = null;
            }

            if (exclusiveInitKey != null) {
                keyEntry.setData(exclusiveInitKey);
                result = DbInternal.search(
                    cursor, keyEntry, null, dataEntry,
                    search, lockMode.toReadOptions());
            } else {
                result = cursor.get(keyEntry, dataEntry,
                                    Get.LAST, lockMode.toReadOptions());
            }
            processResult(result, dataEntry);
            if (result == null) {
                return false;
            }

            /*
             * Because we did not set the rangeConstraint above, we must check
             * it explicitly here for the first record to be returned.
             */
            if (rangeConstraint != null &&
                !rangeConstraint.inBounds(keyEntry.getData())) {
                result = null;
            }

            /* Now use the rangeConstraint to stop the scan. */
            cursor.setRangeConstraint(rangeConstraint);

            if (allDescendants) {
                return (result != null);
            }
            return handleImmediateChildrenReverse();
        }

        assert(state == State.ALL);
        result = cursor.get(keyEntry, dataEntry,
                            Get.PREV, lockMode.toReadOptions());
        processResult(result, dataEntry);
        if (result == null) {
            return false;
        }

        /* Simple case: collect all descendants. */
        if (allDescendants) {
            return true;
        }

        return handleImmediateChildrenReverse();
    }

    private boolean handleImmediateChildrenReverse() {

        /*
         * To collect immediate children only, we position at the child's
         * predecessor key when we encounter a descendant that is not an
         * immediate child.
         */
        while (result != null) {
            final int nComponents =
                Key.countComponents(keyEntry.getData());
            if (nComponents == nChildComponents) {
                /* This is an immediate child. */
                return true;
            }
            /*
             * Not an immediate child.  Move back/up to the immediate
             * child, which has a key that is a prefix of the key we
             * found but with nChildComponents.
             */
            assert nComponents > nChildComponents;
            final byte[] nonChildKey = keyEntry.getData();
            keyEntry.setData
                (Key.getPrefixKey(nonChildKey, nChildComponents));
            result = cursor.get(keyEntry, dataEntry,
                                Get.SEARCH_GTE, lockMode.toReadOptions());
            processResult(result, dataEntry);
            if (result != null &&
                Arrays.equals(nonChildKey, keyEntry.getData())) {

                /*
                 * Arrived back at the same record, meaning that there
                 * is no key with nChildComponents for this child and
                 * we are at the first descendent for this child.  Move
                 * to the previous child.
                 */
                result = cursor.get(keyEntry, dataEntry,
                                    Get.PREV, lockMode.toReadOptions());
                processResult(result, dataEntry);
            }
        }
        return false;
    }

    /*
     * Utility functions.
     */

    /**
     * Returns the first key that sorts higher than a child key, if we were to
     * remove all components following the child key in the given full key.
     *
     * The character with value 1 has the lowest sort value of any character in
     * the "Modified UTF-8" encoding.
     */
    private void getChildKeySuccessor(DatabaseEntry key, byte[] parent) {
        final byte[] bytes = key.getData();

        /* Child begins after the parent's delimiter byte. */
        final int childOff = (parent != null) ? (parent.length + 1) : 0;
        final int childLen = Key.getComponentLength(bytes, childOff);

        /* Successor key is child key plus a byte '1' value. */
        final int newLen = childOff + childLen + 1;
        assert (newLen <= bytes.length);
        bytes[newLen - 1] = MIN_VALUE_BYTE;
        key.setSize(newLen);
    }

    /**
     * Returns the first key that sorts higher than the given prefix, with the
     * same number of bytes.  Searching for a record LT than the return value
     * will find the last (maximum) key having the given prefix.
     *
     * The algorithm increments the least significant byte value. If adding one
     * reaches the max value, set that byte to the min value and adds one to
     * the next most significant byte, etc. If the most significant byte would
     * wrap, then the original prefix consists of all max value bytes, and null
     * is returned. In that case, the keys having the prefix are the last keys
     * in the DB, and Cursor.getLast will land on the last key with the prefix.
     *
     * Note that this method produces a key that may be invalid, i.e., may not
     * be deserializable. This is because its only requirement is to produce a
     * key that compares greater than all keys with the given prefix, without
     * regard to whether the byte values in the result are valid UTF8.
     */
    private static byte[] getPrefixSuccessor(byte[] prefix) {

        final byte[] bytes = new byte[prefix.length];
        System.arraycopy(prefix, 0, bytes, 0, bytes.length);

        for (int i = bytes.length - 1; i >= 0; i -= 1) {
            final int b = bytes[i] & 0xFF;
            if (b == MAX_KEY_BYTE) {
                bytes[i] = (byte) MIN_KEY_BYTE;
                continue;
            }
            bytes[i] = (byte) ((b == MIN_KEY_BYTE) ? 0 : (b + 1));
            return bytes;
        }

        return null;
    }
    
    /**
     * Returns the smallest possible key that sorts higher than the given key.
     *
     * This is similar to calling
     *  Key.addComponent(key, majorPathComplete, "")
     * But knowing majorPathComplete is unnecessary.
     *
     * Note that this method produces a key that may be invalid in the sense
     * that a major path separator is added, which may be inappropriate for
     * the given key. The result should normally not be be de-serialized, and
     * is intended only for comparisons.
     */
    private static byte[] getKeySuccessor(byte[] key) {

        final byte[] bytes = new byte[key.length + 1];
        System.arraycopy(key, 0, bytes, 0, key.length);
        bytes[key.length] = (byte) MIN_KEY_BYTE;

        return bytes;
    }

    /**
     * Returns a RangeConstraint that prevents the cursor from moving past
     * the descendants of the prefix key.
     */
    private RangeConstraint getPrefixConstraint(final byte[] prefixKey) {

        final int prefixLen = prefixKey.length;

        return new RangeConstraint() {
            @Override
            public boolean inBounds(byte[] checkKey) {
                if (checkKey.length < prefixLen) {
                    return false;
                }
                for (int i = 0; i < prefixLen; i += 1) {
                    if (prefixKey[i] != checkKey[i]) {
                        return false;
                    }
                }
                return true;
            }
        };
    }

    /**
     * Returns a RangeConstraint that prevents the cursor from moving backward
     * past an inclusive start point.
     */
    private RangeConstraint
        getRangeStartConstraint(final byte[] startKeyInclusive) {

        return new RangeConstraint() {
            @Override
            public boolean inBounds(byte[] checkKey) {
                return KEY_BYTES_COMPARATOR.compare(checkKey,
                                                    startKeyInclusive) >= 0;
            }
        };
    }

    /**
     * Returns the first string that sorts higher than the given string, if
     * the strings were converted to UTF-8.
     *
     * The character with value 1 has the lowest sort value of any character in
     * the "Modified UTF-8" encoding.
     */
    private String getPathComponentSuccessor(String comp) {
        return comp + MIN_VALUE_CHAR;
    }

    /**
     * Returns a RangeConstraint that prevents the cursor from moving forward
     * past an exclusive end point.
     */
    private RangeConstraint
        getRangeEndConstraint(final byte[] endKeyExclusive) {

        return new RangeConstraint() {
            @Override
            public boolean inBounds(byte[] checkKey) {
                return KEY_BYTES_COMPARATOR.compare(checkKey,
                                                    endKeyExclusive) < 0;
            }
        };
    }

    /**
     * Processes the result of a operation, recording the read cost depending
     * on the success/failure and the data returned. Returns true if the
     * operation was successful (r != null).
     */
    private boolean processResult(OperationResult r, DatabaseEntry e) {
        return processResult(r, e, false);
    }

    private boolean processResult(OperationResult r, DatabaseEntry e,
                                  boolean noChargeEmpty) {
        /* If failure charge the min. read */
        if (r == null) {
            /* If noChargeEmpty is true, ignore charging the min. read */
            if (!noChargeEmpty) {
                op.addEmptyReadCharge();
            }
            return false;
        }

        /*
         * If no data returned, charge min. read if chargeKeyRead is true.
         * Otherwise, get the size of the record.
         */
        if ((e == null) || e.getPartial()) {
            if (chargeKeyRead) {
                op.addReadBytes(MIN_READ);
            }
        } else {
            op.addReadBytes(getStorageSize(cursor));
        }
        return true;
    }

    public void setChargeKeyRead(boolean enable) {
        chargeKeyRead = enable;
    }
    
    /**
     * Initiates asynchronous discarding of records from the specified table.
     * 
     * @throws DatabaseNotFoundException if any name in dbNames does not
     * refer to an existing DB
     */
    public static void discardTableRecords(final Environment env,
                                           final Transaction txn,
                                           final Set dbNames,
                                           final TableImpl table) {
        final byte[] topIdBytes = table.getTopLevelTable().getIDBytes();
        final byte[] startKey = getKeySuccessor(topIdBytes);
        final byte[] endKey = getPrefixSuccessor(topIdBytes);

        /*
         * A table pattern is needed to match child tables, since the key
         * range includes all records in the top-level table.
         */
        final byte[][] tablePattern = (table.getParent() != null) ?
            Scanner.createTablePattern(table) : null;

        /*
         * In the unusual case that endKey is null (see getPrefixSuccessor)
         * we must use a key prefix to terminate the scan.
         */
        final byte[] prefix = (endKey == null) ? topIdBytes : null;

        /*
         * Avoiding a scan filter is desirable, since it adds some storage and
         * processing overhead. But we need a filter if we have to check the
         * key prefix or select child table records.
         */
        final ScanFilter filter = (prefix != null || tablePattern != null) ?
                                new TableFilter(prefix, tablePattern) : null;

        env.discardExtinctRecords(txn, dbNames, new DatabaseEntry(startKey),
                                  (endKey != null) ? new DatabaseEntry(endKey) :
                                                     null,
                                  filter, "DropTable:" + table.getFullName());
    }

    /**
     * Creates a pattern for the given table that can be used to determine
     * whether a key is in the table.
     *
     * The pattern is an array with an element for each key component. The
     * array positions corresponding to table IDs contain the table ID itself
     * as a byte array. All other array positions contain null.
     *
     * @see #matchTablePattern
     */
    private static byte[][] createTablePattern(TableImpl table) {
        final byte[][] pattern = new byte[table.getNumKeyComponents()][];
        do {
            final TableImpl parent = (TableImpl) table.getParent();
            final int i = (parent != null) ? parent.getNumKeyComponents() : 0;
            pattern[i] = table.getIDBytes();
            table = parent;
        } while (table != null);

        return pattern;
    }

    /**
     * Returns whether the given key matches the given pattern, which
     * determines whether the key belongs to the table from which the pattern
     * was created.
     *
     * To match, the key's number of components must equal the number of
     * elements in the pattern. The non-null components in the pattern are
     * matched against table IDs, while the null components are wildcards.
     *
     * @see #createTablePattern
     */
    private static boolean matchTablePattern(byte[] key, byte[][] pattern) {
        int prevOff = 0;

        for (final byte[] comp : pattern) {
            final int nextOff = Key.findNextComponent(key, prevOff);

            if (nextOff < 0) {
                /* Key has less components than the pattern. */
                return false;
            }

            if (comp != null) {
                final int len = nextOff - prevOff;
                if (len != comp.length) {
                    return false;
                }
                for (int i = 0; i < len; i += 1) {
                    if (key[prevOff + i] != comp[i]) {
                        return false;
                    }
                }
            }
            prevOff = nextOff + 1;
        }

        /* Check that there are no extra key components. */
        return Key.findNextComponent(key, prevOff) < 0;
    }

    /**
     * A JE ScanFilter for matching keys in a given table.
     */
    private static class TableFilter implements ScanFilter, Serializable {

        private static final long serialVersionUID = 1L;

        private final byte[] keyPrefix;
        private final byte[][] tablePattern;

        /**
         * @param keyPrefix is the prefix for all keys in the table (the ID
         * ID bytes of the top level table), and is used to stop the scan
         * when a key in a different top-level table is encountered. It may be
         * null if an end key is used to terminate the scan.
         *
         * @param tablePattern is a pattern for matching a particular parent
         * or child table (created by {@link #createTablePattern}, or null
         * to match all records of the top-level table and its child tables.
         */
        TableFilter(byte[] keyPrefix, byte[][] tablePattern) {
            this.keyPrefix = keyPrefix;
            this.tablePattern = tablePattern;
        }

        /**
         * Returns EXCLUDE_STOP to stop the scan when the given key does not
         * have the given keyPrefix (if any).
         *
         * Otherwise, returns EXCLUDE when the key does not match the
         * tablePattern (if any).
         *
         * Otherwise, returns INCLUDE.
         */
        @Override
        public ScanFilter.ScanResult checkKey(byte[] key) {
            if (keyPrefix != null) {
                if (key.length < keyPrefix.length) {
                    return ScanFilter.ScanResult.EXCLUDE_STOP;
                }
                for (int i = 0; i < keyPrefix.length; i += 1) {
                    if (key[i] != keyPrefix[i]) {
                        return ScanFilter.ScanResult.EXCLUDE_STOP;
                    }
                }
            }
            if (tablePattern != null) {
                if (!matchTablePattern(key, tablePattern)) {
                    return ScanFilter.ScanResult.EXCLUDE;
                }
            }
            return ScanFilter.ScanResult.INCLUDE;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy