org.firebirdsql.jdbc.FBServerScrollFetcher Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jaybird Show documentation
Show all versions of jaybird Show documentation
JDBC Driver for the Firebird RDBMS
/*
* Firebird Open Source JDBC Driver
*
* Distributable under LGPL license.
* You may obtain a copy of the License at http://www.gnu.org/copyleft/lgpl.html
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* LGPL License for more details.
*
* This file was created by members of the firebird development team.
* All individual contributions remain the Copyright (C) of those
* individuals. Contributors to this file are either listed here or
* can be obtained from a source control history command.
*
* All rights reserved.
*/
package org.firebirdsql.jdbc;
import org.firebirdsql.gds.JaybirdErrorCodes;
import org.firebirdsql.gds.ng.CursorFlag;
import org.firebirdsql.gds.ng.FbExceptionBuilder;
import org.firebirdsql.gds.ng.FbStatement;
import org.firebirdsql.gds.ng.FetchDirection;
import org.firebirdsql.gds.ng.FetchType;
import org.firebirdsql.gds.ng.LockCloseable;
import org.firebirdsql.gds.ng.fields.RowValue;
import org.firebirdsql.gds.ng.listeners.StatementListener;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import static org.firebirdsql.gds.ISCConstants.INF_RECORD_COUNT;
import static org.firebirdsql.gds.ISCConstants.isc_info_end;
import static org.firebirdsql.gds.VaxEncoding.iscVaxInteger;
import static org.firebirdsql.gds.VaxEncoding.iscVaxInteger2;
/**
* Fetcher implementation for server-side scrollable cursors.
*
* @author Mark Rotteveel
* @since 5
*/
final class FBServerScrollFetcher implements FBFetcher {
private static final int CURSOR_SIZE_UNKNOWN = -1;
private final FbStatement stmt;
private final int maxRows;
private FBObjectListener.FetcherListener fetcherListener;
private int fetchSize;
private boolean closed;
// We delay knowing the local and server-side sizes. Requesting the server-side cursor size triggers a full
// materialization of the cursor on the server, so delaying this avoids overhead at the cost of some more complexity
// in this fetcher.
// The cursor size taking account maxRows
private int cursorSize = CURSOR_SIZE_UNKNOWN;
// The cursor size on the server (ignoring maxRows)
private int serverCursorSize = CURSOR_SIZE_UNKNOWN;
private int serverPosition;
private int localPosition;
// Current window of rows
private List rows = new ArrayList<>();
// Offset of first row in rows
private int rowsOffset;
FBServerScrollFetcher(int initialFetchSize, int maxRows, FbStatement stmt,
FBObjectListener.FetcherListener fetcherListener) throws SQLException {
if (!stmt.supportsFetchScroll()) {
throw new FBDriverNotCapableException("Statement implementation does not support server-side scrollable result sets; this exception indicates a bug in Jaybird");
}
if (!stmt.isCursorFlagSet(CursorFlag.CURSOR_TYPE_SCROLLABLE)) {
throw new FBDriverNotCapableException("Statement does not have CURSOR_TYPE_SCROLLABLE; this exception indicates a bug in Jaybird");
}
fetchSize = initialFetchSize;
this.maxRows = maxRows;
this.stmt = stmt;
this.fetcherListener = fetcherListener;
}
private boolean inWindow(int position) throws SQLException {
int windowSize = rows.size();
int rowsOffset = this.rowsOffset;
if (windowSize == 0 || rowsOffset == 0 || (maxRows != 0 && position > requireCursorSize())) {
return false;
}
return rowsOffset <= position && position < rowsOffset + windowSize;
}
private RowValue rowChange(int newLocalPosition) throws SQLException {
localPosition = newLocalPosition;
return inWindow(newLocalPosition) ? rows.get(newLocalPosition - rowsOffset) : null;
}
private boolean notifyRowChange(int newLocalPosition) throws SQLException {
return notifyRow(rowChange(newLocalPosition));
}
private boolean notifyRow(RowValue rowValue) throws SQLException {
fetcherListener.rowChanged(this, rowValue);
return rowValue != null;
}
private RowListener fetchWithListener(FetchType fetchType, int fetchSize, int position)
throws SQLException {
RowListener rowListener = new RowListener();
stmt.addStatementListener(rowListener);
try {
stmt.fetchScroll(fetchType, fetchSize, position);
} finally {
stmt.removeStatementListener(rowListener);
}
return rowListener;
}
private void updateWindow(RowListener listener, ServerPositionCalculation serverPositionCalculation,
FetchDirection fetchDirection) throws SQLException {
rows.clear();
List newRows = listener.rowValues;
int rowCount = newRows.size();
int serverPosition = this.serverPosition = serverPositionCalculation.newServerPosition(rowCount, listener);
if (rowCount == 0) {
rowsOffset = 0;
} else {
// NOTE: This is safe as long as the fetcher isn't closed, and afterwards, this shouldn't get called
ArrayList rows = (ArrayList) this.rows;
rows.ensureCapacity(rowCount);
if (fetchDirection == FetchDirection.REVERSE) {
if (rowCount == 1) {
rows.add(newRows.get(0));
} else {
// Rows are received from high to low, but we need them from low to high
ListIterator iter = newRows.listIterator(rowCount);
while (iter.hasPrevious()) {
rows.add(iter.previous());
}
}
rowsOffset = listener.beforeFirst ? 1 : serverPosition;
} else {
rows.addAll(newRows);
rowsOffset = serverPosition - rowCount + (listener.afterLast ? 0 : 1);
}
}
}
private void synchronizeServerPosition(int expectedPosition) throws SQLException {
if (serverPosition != expectedPosition) {
stmt.fetchScroll(FetchType.ABSOLUTE, -1, expectedPosition);
serverPosition = expectedPosition;
}
}
@Override
public boolean first() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkOpen();
int newLocalPosition = 1;
if (!inWindow(newLocalPosition) && cursorSize != 0) {
RowListener listener = fetchWithListener(FetchType.FIRST, -1, -1);
updateWindow(listener, (r, l) -> 1, FetchDirection.UNKNOWN);
if (listener.afterLast) {
cursorSize = 0;
}
}
return notifyRowChange(newLocalPosition);
}
}
@Override
public boolean last() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkOpen();
int cursorSize = this.cursorSize;
int newLocalPosition;
if (cursorSize == 0) {
newLocalPosition = 1;
} else if (inWindow(cursorSize)) {
newLocalPosition = cursorSize;
} else {
RowListener listener;
if (maxRows != 0 && ((cursorSize = requireCursorSize()) < requireServerCursorSize())) {
listener = fetchWithListener(FetchType.ABSOLUTE, -1, cursorSize);
} else {
listener = fetchWithListener(FetchType.LAST, -1, -1);
}
updateWindow(listener, lastServerPosition(), FetchDirection.UNKNOWN);
newLocalPosition = serverPosition;
}
return notifyRowChange(newLocalPosition);
}
}
private ServerPositionCalculation lastServerPosition() {
return (rowCount, listener) -> rowCount == 0 ? 1 : requireCursorSize();
}
@Override
public boolean previous() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkOpen();
int oldLocalPosition = localPosition;
int newLocalPosition = Math.max(1, oldLocalPosition) - 1;
if (!inWindow(newLocalPosition)) {
synchronizeServerPosition(oldLocalPosition);
RowListener listener = fetchWithListener(FetchType.PRIOR, actualFetchSize(), -1);
updateWindow(listener, previousServerPosition(oldLocalPosition), FetchDirection.REVERSE);
if (listener.beforeFirst) {
serverPosition = 0;
}
}
return notifyRowChange(newLocalPosition);
}
}
private ServerPositionCalculation previousServerPosition(int initialServerPosition) {
return (rowCount, listener) -> listener.beforeFirst ? 0 : initialServerPosition - rowCount;
}
@Override
public boolean next() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkOpen();
int oldLocalPosition = localPosition;
boolean hasMaxRows = maxRows > 0;
int cursorSize = hasMaxRows && oldLocalPosition != 0 ? requireCursorSize() : this.cursorSize;
int newLocalPosition =
(cursorSize != CURSOR_SIZE_UNKNOWN ? Math.min(cursorSize, oldLocalPosition) : oldLocalPosition) + 1;
if (!inWindow(newLocalPosition)) {
int fetchSize = actualFetchSize();
if (hasMaxRows) {
if (cursorSize == CURSOR_SIZE_UNKNOWN) {
// special case if not fetched yet while maxRows is set
fetchSize = Math.min(fetchSize, maxRows);
} else {
fetchSize = Math.min(fetchSize, cursorSize + 1 - oldLocalPosition);
}
}
if (fetchSize == 0) {
afterLast();
return false;
}
synchronizeServerPosition(oldLocalPosition);
RowListener listener = fetchWithListener(FetchType.NEXT, fetchSize, -1);
updateWindow(listener, nextServerPosition(oldLocalPosition), FetchDirection.FORWARD);
}
return notifyRowChange(newLocalPosition);
}
}
private ServerPositionCalculation nextServerPosition(int initialServerPosition) {
return (rowCount, listener) -> listener.afterLast ? requireCursorSize() + 1 : initialServerPosition + rowCount;
}
@Override
public boolean absolute(int row) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkOpen();
// Overflow beyond cursor size is handled by inWindow returning false
int newLocalPosition = row >= 0 ? row : Math.max(0, requireCursorSize() + 1 + row);
if (!inWindow(row)) {
if (maxRows != 0 && newLocalPosition > requireCursorSize()) {
afterLast();
return false;
}
// NOTE: Using the desired position instead of row to avoid issues when maxRow is set,
// so not using negative fetch offered by server-side cursor
RowListener listener = fetchWithListener(FetchType.ABSOLUTE, -1, newLocalPosition);
FetchDirection fetchDirection = row < 0 ? FetchDirection.REVERSE : FetchDirection.FORWARD;
updateWindow(listener, absoluteServerPosition(row), fetchDirection);
newLocalPosition = serverPosition;
}
return notifyRowChange(newLocalPosition);
}
}
private ServerPositionCalculation absoluteServerPosition(int absoluteRow) {
return (rowCount, listener) -> {
if (listener.beforeFirst) {
return 0;
} else if (listener.afterLast) {
return requireCursorSize() + 1;
} else if (absoluteRow >= 0) {
// case absoluteRow == 0 should be handled by if (listener.beforeFirst), but covered for completeness
return absoluteRow;
} else {
return requireCursorSize() + absoluteRow + 1;
}
};
}
@Override
public boolean relative(int row) throws SQLException {
try (LockCloseable ignored = withLock()) {
checkOpen();
int oldLocalPosition = localPosition;
// Overflow beyond cursor size is handled by inWindow returning false
int newLocalPosition = Math.max(0, oldLocalPosition + row);
if (row != 0 && !inWindow(newLocalPosition)) {
if (maxRows != 0 && newLocalPosition > requireCursorSize()) {
afterLast();
return false;
}
synchronizeServerPosition(oldLocalPosition);
RowListener listener = fetchWithListener(FetchType.RELATIVE, -1, row);
FetchDirection fetchDirection = row < 0 ? FetchDirection.REVERSE : FetchDirection.FORWARD;
updateWindow(listener, relativeServerPosition(row), fetchDirection);
newLocalPosition = serverPosition;
}
return notifyRowChange(newLocalPosition);
}
}
private ServerPositionCalculation relativeServerPosition(int relativeRow) {
return (rowCount, listener) -> {
if (listener.beforeFirst) {
return 0;
} else if (listener.afterLast) {
// NOTE: If not for the row != 0 check in relative(int), this would require specific handling
// for serverPosition == 0 and relativeRow == 0 given the handling of IN_PLACE in V10Statement.processFetchResponse
return requireCursorSize() + 1;
} else {
assert rowCount == 1 : "expected rowCount == 1, was " + rowCount;
return serverPosition + relativeRow;
}
};
}
@Override
public void beforeFirst() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkOpen();
if (localPosition != 0) {
stmt.fetchScroll(FetchType.ABSOLUTE, -1, 0);
serverPosition = 0;
}
notifyRowChange(0);
}
}
@Override
public void afterLast() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkOpen();
int afterLastPosition = requireCursorSize() + 1;
if (localPosition != afterLastPosition) {
stmt.fetchScroll(FetchType.ABSOLUTE, -1, afterLastPosition);
serverPosition = afterLastPosition;
}
notifyRowChange(afterLastPosition);
}
}
@Override
public void close() throws SQLException {
close(CompletionReason.OTHER);
}
@Override
public void close(CompletionReason completionReason) throws SQLException {
closed = true;
try {
stmt.closeCursor(completionReason.isTransactionEnd() || completionReason.isCompletesStatement());
} finally {
rows.clear();
rowsOffset = 0;
rows = Collections.emptyList();
fetcherListener.fetcherClosed(this);
}
}
private void checkOpen() throws SQLException {
if (closed) throw new FBSQLException("Result set is already closed.");
}
@Override
public int getRowNum() throws SQLException {
try (LockCloseable ignored = withLock()) {
// NOTE Relying on isAfterLast to (indirectly) call checkOpen()
return isAfterLast() ? 0 : localPosition;
}
}
@Override
public boolean isEmpty() throws SQLException {
try (LockCloseable ignored = withLock()) {
// NOTE Relying on requireCursorSize to call checkOpen()
int cursorSize = requireCursorSize();
return cursorSize == 0;
}
}
@Override
public boolean isBeforeFirst() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkOpen();
return localPosition == 0;
}
}
@Override
public boolean isFirst() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkOpen();
return localPosition == 1 && requireCursorSize() > 0;
}
}
@Override
public boolean isLast() throws SQLException {
try (LockCloseable ignored = withLock()) {
// NOTE Relying on requireCursorSize to call checkOpen()
int cursorSize = requireCursorSize();
return localPosition == cursorSize && cursorSize > 0;
}
}
@Override
public boolean isAfterLast() throws SQLException {
try (LockCloseable ignored = withLock()) {
if (localPosition == 0) return false;
// NOTE Relying on requireCursorSize to call checkOpen()
int cursorSize = requireCursorSize();
return localPosition > cursorSize;
}
}
@Override
public void beforeExecuteInsert() throws SQLException {
requireCursorSize();
}
@Override
public void insertRow(RowValue data) throws SQLException {
throw new UnsupportedOperationException("Implementation error: FBServerScrollFetcher should be decorated with FBUpdatableFetcher");
}
@Override
public void deleteRow() throws SQLException {
throw new UnsupportedOperationException("Implementation error: FBServerScrollFetcher should be decorated with FBUpdatableFetcher");
}
@Override
public void updateRow(RowValue data) throws SQLException {
throw new UnsupportedOperationException("Implementation error: FBServerScrollFetcher should be decorated with FBUpdatableFetcher");
}
private int actualFetchSize() {
return fetchSize > 0 ? fetchSize : DEFAULT_FETCH_ROWS;
}
@Override
public int getFetchSize() throws SQLException {
try (LockCloseable ignored = withLock()) {
checkOpen();
return fetchSize;
}
}
@Override
public void setFetchSize(int fetchSize) {
try (LockCloseable ignored = withLock()) {
this.fetchSize = fetchSize;
}
}
@Override
public int currentPosition() {
return localPosition;
}
@Override
public int size() throws SQLException {
return requireCursorSize();
}
@Override
public void setFetcherListener(FBObjectListener.FetcherListener fetcherListener) {
this.fetcherListener = fetcherListener;
}
private int retrieveServerCursorSize() throws SQLException {
return stmt.getCursorInfo(new byte[] { (byte) INF_RECORD_COUNT, isc_info_end }, 10, buffer -> {
if (buffer[0] != INF_RECORD_COUNT) {
throw FbExceptionBuilder.forException(JaybirdErrorCodes.jb_infoResponseEmpty)
.messageParameter("cursor")
.toSQLException();
}
int length = iscVaxInteger2(buffer, 1);
return iscVaxInteger(buffer, 3, length);
});
}
private int requireCursorSize() throws SQLException {
checkOpen();
int cursorSize = this.cursorSize;
if (cursorSize == CURSOR_SIZE_UNKNOWN) {
int serverCursorSize = requireServerCursorSize();
cursorSize = this.cursorSize = maxRows == 0 ? serverCursorSize : Math.min(maxRows, serverCursorSize);
}
return cursorSize;
}
private int requireServerCursorSize() throws SQLException{
int serverCursorSize = this.serverCursorSize;
if (serverCursorSize != CURSOR_SIZE_UNKNOWN) return serverCursorSize;
if (!stmt.hasFetched()) {
// A fetch is required before we can retrieve the cursor size, fetch without moving current position
stmt.fetchScroll(FetchType.RELATIVE, -1, 0);
}
return this.serverCursorSize = retrieveServerCursorSize();
}
private LockCloseable withLock() {
return stmt.withLock();
}
private static final class RowListener implements StatementListener {
boolean beforeFirst;
boolean afterLast;
final List rowValues = new ArrayList<>();
@Override
public void beforeFirst(FbStatement sender) {
beforeFirst = true;
}
@Override
public void afterLast(FbStatement sender) {
afterLast = true;
}
@Override
public void receivedRow(FbStatement sender, RowValue rowValue) {
rowValues.add(rowValue);
}
}
@FunctionalInterface
private interface ServerPositionCalculation {
int newServerPosition(int receivedRowCount, RowListener listener) throws SQLException;
}
}