
oracle.kv.impl.api.ops.IndexScanner Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of oracle-nosql-server Show documentation
Show all versions of oracle-nosql-server Show documentation
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 com.sleepycat.je.CursorConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DbInternal;
import com.sleepycat.je.DbInternal.Search;
import com.sleepycat.je.Get;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationResult;
import com.sleepycat.je.SecondaryCursor;
import com.sleepycat.je.SecondaryDatabase;
import com.sleepycat.je.Transaction;
import oracle.kv.impl.api.table.IndexImpl;
import oracle.kv.impl.api.table.IndexKeyImpl;
import oracle.kv.impl.api.table.IndexRange;
import oracle.kv.impl.util.TxnUtil;
/**
* A class to encapsulate index iteration over secondary databases.
*
* The usage pattern is:
* IndexScanner scanner = new IndexScanner(...);
* try {
* while (scanner.next()) {
* DatabaseEntry indexKeyEntry = scanner.getIndexKey();
* DatabaseEntry primaryKeyEntry = scanner.getPrimaryKey();
* DatabaseEntry dataEntry = scanner.getData();
* // do things with indexKeyEntry, primaryKeyEntry, dataEntry
* }
* } finally {
* scanner.close(); // closes cursor
* }
*
* The next() call returns false when there are no more entries in range.
*
* Any security-relevant operations are performed by callers. Batching is
* also handled by callers.
*/
public class IndexScanner {
private final InternalOperation op;
private final IndexImpl index;
private final IndexRange range;
private final byte[] resumeSecondaryKey;
private final byte[] resumePrimaryKey;
/*
* 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 SecondaryCursor cursor;
private final DatabaseEntry dataEntry;
private final LockMode lockMode;
/*
* These are "final" once initialized
*/
private DatabaseEntry indexKeyEntry;
private DatabaseEntry primaryKeyEntry;
private boolean initialized = false;
private OperationResult result;
IndexScanner(InternalOperation op,
Transaction txn,
SecondaryDatabase db,
IndexImpl index,
IndexRange range,
byte[] resumeSecondaryKey,
byte[] resumePrimaryKey,
boolean moveAfterResumeKey,
boolean keyOnly) {
this(op, txn, db, index, range, resumeSecondaryKey,
resumePrimaryKey, moveAfterResumeKey,
OperationHandler.CURSOR_READ_COMMITTED,
LockMode.DEFAULT, keyOnly);
}
IndexScanner(InternalOperation op,
Transaction txn,
SecondaryDatabase db,
IndexImpl index,
IndexRange range,
byte[] resumeSecondaryKey,
byte[] resumePrimaryKey,
boolean moveAfterResumeKey,
CursorConfig cursorConfig,
LockMode lockMode,
boolean keyOnly) {
this.op = op;
this.index = index;
this.range = range;
this.resumeSecondaryKey = resumeSecondaryKey;
this.resumePrimaryKey = resumePrimaryKey;
this.moveAfterResumeKey = moveAfterResumeKey;
this.lockMode = lockMode;
dataEntry = (keyOnly ? null : new DatabaseEntry());
cursor = db.openCursor(txn, cursorConfig);
}
/**
* Returns the index key
*/
public final DatabaseEntry getIndexKey() {
return indexKeyEntry;
}
/**
* Returns the primary key
*/
public final DatabaseEntry getPrimaryKey() {
return primaryKeyEntry;
}
/**
* Returns the data
*/
public final DatabaseEntry getData() {
return dataEntry;
}
/**
* Returns the cursor
*/
public final SecondaryCursor getCursor() {
return cursor;
}
/**
* This is called to lock the current data entry for key-only scans.
*
* This method is called after getNext() method. We don't need to record
* any read cost here because the LN is not read and the cost of reading
* the index entry was recorded in the getNext() method.
*/
public boolean getCurrent() {
assert(dataEntry == null);
result = cursor.get(indexKeyEntry,
primaryKeyEntry,
dataEntry,
Get.CURRENT,
LockMode.DEFAULT.toReadOptions());
return result != null;
}
/**
* This is called if this.dataEntry is partial and the data needs to be
* locked and fetched.
*/
public boolean getLockedData(DatabaseEntry newEntry) {
assert !newEntry.getPartial();
result = cursor.get(indexKeyEntry,
primaryKeyEntry,
newEntry,
Get.CURRENT,
LockMode.DEFAULT.toReadOptions());
return processResult(result, newEntry);
}
public boolean lockIndexEntry() {
result = cursor.get(indexKeyEntry,
null, /* primaryKeyEntry */
Get.CURRENT,
LockMode.DEFAULT.toReadOptions());
return result != null;
}
/**
* 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);
}
/*
* Gets the next, or previous entry and checks range, returning true if
* there is a "next" key and it is in range.
*/
public boolean next() {
boolean haveKey;
/*
* Delay adding the read cost generated from getPrev()/getNext() until
* the index key is checked range in inRange() method, if the index
* entry is in range, add the read cost to throughput tracking,
* otherwise ignore the read cost.
*/
op.beginAddReadBytes();
if (range.isReverse()) {
haveKey = getPrev();
} else {
haveKey = getNext();
}
if (haveKey) {
boolean ret = inRange(indexKeyEntry.getData());
op.flushReadBytes(ret);
if (!ret) {
/* No matched key found, charge empty read */
op.addEmptyReadCharge();
}
return ret;
}
op.flushReadBytes(true);
return false;
}
/*
* Forward scans
*/
private boolean getNext() {
if (!initialized) {
initialized = true;
return initForward();
}
if (range.getExactMatch()) {
result = cursor.get(indexKeyEntry,
primaryKeyEntry,
dataEntry,
Get.NEXT_DUP,
lockMode.toReadOptions());
} else {
result = cursor.get(indexKeyEntry,
primaryKeyEntry,
dataEntry,
Get.NEXT,
lockMode.toReadOptions());
}
return processResult(result, dataEntry);
}
private boolean initForward() {
byte[] startKey = range.getStartKey();
/*
* If resuming an iteration, start there. It overrides the
* startKey. A resumeKey doesn't mean that it isn't an exact
* match query. It's possible that batch size is < number of
* duplicates.
*/
if (resumeSecondaryKey != null) {
indexKeyEntry = new DatabaseEntry(resumeSecondaryKey);
primaryKeyEntry = new DatabaseEntry(resumePrimaryKey);
result = resume();
return (result != null);
}
if (startKey != null) {
indexKeyEntry = new DatabaseEntry(startKey);
primaryKeyEntry = new DatabaseEntry();
if (range.getExactMatch()) {
result = cursor.get(indexKeyEntry,
primaryKeyEntry,
dataEntry,
Get.SEARCH,
lockMode.toReadOptions());
} else {
result = cursor.get(indexKeyEntry,
primaryKeyEntry,
dataEntry,
Get.SEARCH_GTE,
lockMode.toReadOptions());
}
} else {
/*
* This is a full index scan.
*
* If somehow an invalid IndexRange was sent and exact match is
* true in this path, return no entries.
*/
if (range.getExactMatch()) {
return false;
}
indexKeyEntry = new DatabaseEntry();
primaryKeyEntry = new DatabaseEntry();
result = cursor.get(indexKeyEntry,
primaryKeyEntry,
dataEntry,
Get.FIRST,
lockMode.toReadOptions());
}
return processResult(result, dataEntry);
}
/**
* Do a reverse index scan. Positioning of the cursor is tricky
* for this case. "start" is the end of the scan and "end" is the start.
* A resumeKey is equivalent to the end (which is inclusive).
* 1. No range constraints. Start at the end and move backwards. This
* happens when there is no end key and no prefix.
* 2. Start only. Start at the end and move backwards to start. This
* happens when start is set and there is no prefix
* 3. End only. Find end, move backwards. This happens when there is
* an end key and no prefix.
* 4. Prefix serves as both start and/or end. When specified with a
* start key only it is used to find the end of the range. When specified
* with an end key it is used to terminate the iteration. When specified
* alone it is both the start and the end.
*/
private boolean getPrev() {
if (!initialized) {
initialized = true;
return initReverse();
}
if (range.getExactMatch()) {
result = cursor.get(indexKeyEntry,
primaryKeyEntry,
dataEntry,
Get.PREV_DUP,
lockMode.toReadOptions());
} else {
result = cursor.get(indexKeyEntry,
primaryKeyEntry,
dataEntry,
Get.PREV,
lockMode.toReadOptions());
}
return processResult(result, dataEntry);
}
private boolean initReverse() {
byte[] endKey = range.getEndKey();
if (resumeSecondaryKey != null) {
indexKeyEntry = new DatabaseEntry(resumeSecondaryKey);
primaryKeyEntry = new DatabaseEntry(resumePrimaryKey);
result = resume();
} else if (endKey != null) {
/*
* End keys are exclusive. Move to the prev sec key, and to the
* last dup for that key.
*/
indexKeyEntry = new DatabaseEntry(endKey);
primaryKeyEntry = new DatabaseEntry();
result = DbInternal.search(cursor, indexKeyEntry, primaryKeyEntry,
dataEntry, Search.LT,
lockMode.toReadOptions());
processResult(result, dataEntry);
} else if (range.getPrefixKey() != null) {
indexKeyEntry = new DatabaseEntry();
primaryKeyEntry = new DatabaseEntry();
result = getEndFromPrefix();
} else {
/*
* This is either a complete index iteration or an iteration
* without a bounded end. In both cases start at the last
* record in the index.
*/
indexKeyEntry = new DatabaseEntry();
primaryKeyEntry = new DatabaseEntry();
result = cursor.get(indexKeyEntry,
primaryKeyEntry,
dataEntry,
Get.LAST,
lockMode.toReadOptions());
processResult(result, dataEntry);
}
return result != null;
}
/**
* Moves to the record after (or before, for a reverse scan) the given
* key/pkey.
*/
private OperationResult resume() {
final Search search =
(range.isReverse() ?
(moveAfterResumeKey ? Search.LT : Search.LTE) :
(moveAfterResumeKey ? Search.GT : Search.GTE));
/* First search within dups for the given sec key. */
result = DbInternal.searchBoth(
cursor, indexKeyEntry, primaryKeyEntry, dataEntry, search,
lockMode.toReadOptions());
processResult(result, dataEntry, !range.getExactMatch());
/*
* If NOTFOUND and exact-match then we're done because the search is
* limited to the dups within the given sec key.
*/
if (result != null || range.getExactMatch()) {
return result;
}
/*
* There are no more records with the given sec key. Move to the next
* (or prev) sec key, and to the first (or last) dup for that key.
*/
final OperationResult ret =
DbInternal.search(cursor, indexKeyEntry, primaryKeyEntry,
dataEntry, search, lockMode.toReadOptions());
processResult(ret, dataEntry);
return ret;
}
/**
* Get the last matching record from a prefix. This is a reverse scan
* that has a prefix but no end key so the end is implicitly at the
* end of the prefix. Index field serialization does not include
* explicit separators (like primary key serialization) but it does have
* schema available and prefixes are complete, valid fields. The algorithm
* is:
* 1. deserialize the prefix to IndexKeyImpl
* 2. add "one" to the index key using IndexKeyImpl.incrementLastField().
* 3. reserialize and use this as an exclusive key.
*/
private OperationResult getEndFromPrefix() {
assert range.getPrefixKey() != null;
/* Deserialize */
IndexKeyImpl indexKey = index.deserializeIndexKey(range.getPrefixKey(),
true); // allowPartial
/*
* Increment the last field with a value in the index key. If this
* returns false then there are no further keys so go to the end of
* the index.
*/
if (!indexKey.incrementIndexKey()) {
/* Nothing there, go to the end of the index */
final OperationResult ret =
cursor.get(indexKeyEntry, primaryKeyEntry, dataEntry,
Get.LAST, lockMode.toReadOptions());
processResult(ret, dataEntry);
return ret;
}
/* Reserialize */
byte[] bytes = index.serializeIndexKey(indexKey);
/*
* Look for end. This is an exclusive value. Prefixes in this
* case are, by definition inclusive. End keys can only be
* exclusive if explicitly declared and that path does not call
* this function.
*/
indexKeyEntry.setData(bytes);
/* Match record < exclusive key */
final OperationResult ret =
DbInternal.search(cursor, indexKeyEntry, primaryKeyEntry,
dataEntry, Search.LT,lockMode.toReadOptions());
processResult(ret, dataEntry);
return ret;
}
private boolean inRange(byte[] checkKey) {
return range.inRange(checkKey);
}
/**
* 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) {
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()) {
op.addReadBytes(MIN_READ);
} else {
op.addReadBytes(getStorageSize(cursor));
}
return true;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy