Issue #701: An available index is never used for ordering, when the requested order is DESC
+
Issue #4036: NULLS NOT DISTINCT constraint changed after column dropped
Issue #4033: Wrong array produced when using ARRAY_AGG() on UNNEST(ARRAY[CAST(? AS INT)]) expression
diff --git a/h2/src/docsrc/html/performance.html b/h2/src/docsrc/html/performance.html
index 63ed7322b1..2dd4563bd5 100644
--- a/h2/src/docsrc/html/performance.html
+++ b/h2/src/docsrc/html/performance.html
@@ -732,6 +732,17 @@
Special Optimizations
For queries of the form SELECT * FROM TEST ORDER BY ID, the query plan includes the line
/* index sorted */ to indicate there is no separate sorting required.
+/* index sorted: 2 of 3 columns */ indicates that only some columns are sorted with an index.
+An additional sorting is still required, but queries with the FETCH (TOP, LIMIT) clause
+may still stop their execution earlier.
+An index on (A ASC, B ASC) columns can be used for ORDER BY A, ORDER BY A DESC,
+ORDER BY A, B or ORDER BY A DESC, B DESC.
+With ORDER BY A, B DESC this index can only be used for ordering on the column A.
+If columns are nullable, order of nulls is also important. Index on (A ASC NULLS FIRST) cannot be used for
+ORDER BY A ASC NULLS LAST, but can be used for ORDER BY A ASC NULLS FIRST or
+ORDER BY A DESC NULLS LAST.
+When neither NULLS FIRST nor NULLS LAST is specified, a default is used, this default
+is controlled by DEFAULT_NULL_ORDERING setting.
For queries of the form SELECT * FROM TEST GROUP BY ID ORDER BY ID, the query plan includes the line
/* group sorted */ to indicate there is no separate sorting required.
diff --git a/h2/src/main/org/h2/command/ddl/Analyze.java b/h2/src/main/org/h2/command/ddl/Analyze.java
index 8ce62d1e23..d6c6312332 100644
--- a/h2/src/main/org/h2/command/ddl/Analyze.java
+++ b/h2/src/main/org/h2/command/ddl/Analyze.java
@@ -187,7 +187,7 @@ public static void analyzeTable(SessionLocal session, Table table, int sample, b
if (columnCount == 0) {
return;
}
- Cursor cursor = table.getScanIndex(session).find(session, null, null);
+ Cursor cursor = table.getScanIndex(session).find(session, null, null, false);
if (cursor.next()) {
SelectivityData[] array = new SelectivityData[columnCount];
for (int i = 0; i < columnCount; i++) {
diff --git a/h2/src/main/org/h2/command/dml/ScriptCommand.java b/h2/src/main/org/h2/command/dml/ScriptCommand.java
index d296425964..62e835ea4f 100644
--- a/h2/src/main/org/h2/command/dml/ScriptCommand.java
+++ b/h2/src/main/org/h2/command/dml/ScriptCommand.java
@@ -474,7 +474,7 @@ private static T[] sorted(Collection collection, Class getIndexSorts() {
if (sort == null) {
return null;
}
@@ -597,7 +598,9 @@ private Index getSortIndex() {
int[] queryColumnIndexes = sort.getQueryColumnIndexes();
int queryIndexesLength = queryColumnIndexes.length;
int[] sortIndex = new int[queryIndexesLength];
- for (int i = 0, j = 0; i < queryIndexesLength; i++) {
+ int sortedColumns = 0;
+ boolean needMore = false;
+ for (int i = 0; i < queryIndexesLength; i++) {
int idx = queryColumnIndexes[i];
if (idx < 0 || idx >= expressions.size()) {
throw DbException.getInvalidValueException("ORDER BY", idx + 1);
@@ -608,62 +611,100 @@ private Index getSortIndex() {
continue;
}
if (!(expr instanceof ExpressionColumn)) {
- return null;
+ needMore = true;
+ break;
}
ExpressionColumn exprCol = (ExpressionColumn) expr;
if (exprCol.getTableFilter() != topTableFilter) {
- return null;
+ needMore = true;
+ break;
}
sortColumns.add(exprCol.getColumn());
- sortIndex[j++] = i;
+ sortIndex[sortedColumns++] = i;
}
- Column[] sortCols = sortColumns.toArray(new Column[0]);
- if (sortCols.length == 0) {
+ if (sortedColumns == 0) {
+ if (needMore) {
+ // Can't sort using index
+ return null;
+ }
// sort just on constants - can use scan index
- return topTableFilter.getTable().getScanIndex(session);
+ return List.of(new IndexSort(topTableFilter.getTable().getScanIndex(session), false));
+ }
+ Column[] sortCols;
+ int[] sortTypes = sort.getSortTypesWithNullOrdering();
+ if (sortedColumns == 1) {
+ Column column = sortColumns.get(0);
+ if (column.getColumnId() == -1) {
+ // special case: order by _ROWID_
+ Index index = topTableFilter.getTable().getScanIndex(session);
+ if (index.isRowIdIndex()) {
+ return List.of(new IndexSort(index, needMore ? sortedColumns : IndexSort.FULLY_SORTED,
+ (sortTypes[sortIndex[0]] & SortOrder.DESCENDING) != 0));
+ }
+ }
+ sortCols = new Column[] { column };
+ } else {
+ sortCols = sortColumns.toArray(new Column[0]);
}
ArrayList list = topTableFilter.getTable().getIndexes();
- if (list != null) {
- int[] sortTypes = sort.getSortTypesWithNullOrdering();
- DefaultNullOrdering defaultNullOrdering = getDatabase().getDefaultNullOrdering();
- loop: for (Index index : list) {
- if (index.getCreateSQL() == null) {
- // can't use the scan index
- continue;
- }
- if (index.getIndexType().isHash()) {
- continue;
- }
- IndexColumn[] indexCols = index.getIndexColumns();
- if (indexCols.length < sortCols.length) {
- continue;
- }
- for (int j = 0; j < sortCols.length; j++) {
- // the index and the sort order must start
- // with the exact same columns
- IndexColumn idxCol = indexCols[j];
- Column sortCol = sortCols[j];
- if (idxCol.column != sortCol) {
- continue loop;
+ if (list == null) {
+ return null;
+ }
+ DefaultNullOrdering defaultNullOrdering = getDatabase().getDefaultNullOrdering();
+ ArrayList indexSorts = Utils.newSmallArrayList();
+ loop: for (Index index : list) {
+ if (index.getCreateSQL() == null || index.getIndexType().isHash()) {
+ // can't use scan or hash indexes
+ continue;
+ }
+ IndexColumn[] indexCols = index.getIndexColumns();
+ int count = Math.min(indexCols.length, sortedColumns);
+ boolean reverse = false;
+ for (int j = 0; j < count; j++) {
+ // the index and the sort order must start
+ // with the exact same columns
+ IndexColumn idxCol = indexCols[j];
+ Column sortCol = sortCols[j];
+ boolean mismatch = idxCol.column != sortCol;
+ if (!mismatch) {
+ if (sortCol.isNullable()) {
+ int o1 = defaultNullOrdering.addExplicitNullOrdering(idxCol.sortType);
+ int o2 = sortTypes[sortIndex[j]];
+ if (j == 0) {
+ if (o1 != o2) {
+ if (o1 == SortOrder.inverse(o2)) {
+ reverse = true;
+ } else {
+ mismatch = true;
+ }
+ }
+ } else {
+ if (o1 != (reverse ? SortOrder.inverse(o2) : o2)) {
+ mismatch = true;
+ }
+ }
+ } else {
+ boolean different = (idxCol.sortType & SortOrder.DESCENDING) //
+ != (sortTypes[sortIndex[j]] & SortOrder.DESCENDING);
+ if (j == 0) {
+ reverse = different;
+ } else {
+ mismatch = different != reverse;
+ }
}
- int sortType = sortTypes[sortIndex[j]];
- if (sortCol.isNullable()
- ? defaultNullOrdering.addExplicitNullOrdering(idxCol.sortType) != sortType
- : (idxCol.sortType & SortOrder.DESCENDING) != (sortType & SortOrder.DESCENDING)) {
- continue loop;
+ }
+ if (mismatch) {
+ if (j > 0) {
+ indexSorts.add(new IndexSort(index, j, reverse));
}
+ continue loop;
}
- return index;
- }
- }
- if (sortCols.length == 1 && sortCols[0].getColumnId() == -1) {
- // special case: order by _ROWID_
- Index index = topTableFilter.getTable().getScanIndex(session);
- if (index.isRowIdIndex()) {
- return index;
}
+ indexSorts.add(new IndexSort(index, needMore || count < sortedColumns ? count : IndexSort.FULLY_SORTED,
+ reverse));
}
- return null;
+ indexSorts.sort(null);
+ return indexSorts;
}
private void queryDistinct(ResultTarget result, long offset, long limitRows, boolean withTies,
@@ -683,7 +724,7 @@ private void queryDistinct(ResultTarget result, long offset, long limitRows, boo
if (!quickOffset) {
offset = 0;
}
- while (true) {
+ for (;;) {
setCurrentRowNumber(++rowNumber);
Cursor cursor = index.findNext(session, first, null);
if (!cursor.next()) {
@@ -700,15 +741,16 @@ private void queryDistinct(ResultTarget result, long offset, long limitRows, boo
continue;
}
result.addRow(value);
- if ((sort == null || sortUsingIndex) && limitRows > 0 && rowNumber >= limitRows && !withTies) {
+ if ((sort == null || indexSortedColumns == IndexSort.FULLY_SORTED) && limitRows > 0
+ && rowNumber >= limitRows && !withTies) {
break;
}
}
}
private LazyResult queryFlat(int columnCount, ResultTarget result, long offset, long limitRows, boolean withTies,
- boolean quickOffset) {
- if (limitRows > 0 && offset > 0 && !quickOffset) {
+ QuickOffset quickOffset) {
+ if (limitRows > 0 && offset > 0 && quickOffset != QuickOffset.YES) {
limitRows += offset;
if (limitRows < 0) {
// Overflow
@@ -716,30 +758,47 @@ private LazyResult queryFlat(int columnCount, ResultTarget result, long offset,
}
}
LazyResultQueryFlat lazyResult = new LazyResultQueryFlat(expressionArray, columnCount, forUpdate != null);
- skipOffset(lazyResult, offset, quickOffset);
+ skipOffset(lazyResult, offset, quickOffset == QuickOffset.YES);
if (result == null) {
return lazyResult;
}
- if (limitRows < 0 || sort != null && !sortUsingIndex || withTies && !quickOffset) {
- limitRows = Long.MAX_VALUE;
+ if (limitRows == Long.MAX_VALUE || limitRows < 0 || sort != null && indexSortedColumns == 0
+ || withTies && quickOffset == QuickOffset.NO) {
+ while (lazyResult.next()) {
+ result.addRow(lazyResult.currentRow());
+ }
+ } else {
+ readWithLimit(result, limitRows, withTies, lazyResult);
}
- Value[] row = null;
+ return null;
+ }
+
+ private void readWithLimit(ResultTarget result, long limitRows, boolean withTies, LazyResultQueryFlat lazyResult) {
+ Value[] last = null;
while (result.getRowCount() < limitRows && lazyResult.next()) {
- row = lazyResult.currentRow();
- result.addRow(row);
- }
- if (limitRows != Long.MAX_VALUE && withTies && sort != null && row != null) {
- Value[] expected = row;
- while (lazyResult.next()) {
- row = lazyResult.currentRow();
- if (sort.compare(expected, row) != 0) {
- break;
+ last = lazyResult.currentRow();
+ result.addRow(last);
+ }
+ if (sort != null && last != null) {
+ if (indexSortedColumns < IndexSort.FULLY_SORTED) {
+ while (lazyResult.next()) {
+ Value[] row = lazyResult.currentRow();
+ if (sort.compare(last, row, indexSortedColumns) != 0) {
+ break;
+ }
+ result.addRow(row);
+ }
+ } else if (withTies) {
+ while (lazyResult.next()) {
+ Value[] row = lazyResult.currentRow();
+ if (sort.compare(last, row) != 0) {
+ break;
+ }
+ result.addRow(row);
}
- result.addRow(row);
+ result.limitsWereApplied();
}
- result.limitsWereApplied();
}
- return null;
}
private static void skipOffset(LazyResultSelect lazyResult, long offset, boolean quickOffset) {
@@ -778,22 +837,22 @@ protected ResultInterface queryWithoutCache(long maxRows, ResultTarget target) {
result = createLocalResult(result);
}
// Do not add rows before OFFSET to result if possible
- boolean quickOffset = !fetchPercent;
- if (sort != null && (!sortUsingIndex || isAnyDistinct())) {
+ QuickOffset quickOffset = fetchPercent ? QuickOffset.NO : QuickOffset.YES;
+ if (sort != null && (indexSortedColumns != IndexSort.FULLY_SORTED || isAnyDistinct())) {
result = createLocalResult(result);
result.setSortOrder(sort);
- if (!sortUsingIndex) {
- quickOffset = false;
+ if (indexSortedColumns != IndexSort.FULLY_SORTED) {
+ quickOffset = indexSortedColumns > 0 ? QuickOffset.PARTIAL : QuickOffset.NO;
}
}
if (distinct) {
result = createLocalResult(result);
if (!isDistinctQuery) {
- quickOffset = false;
+ quickOffset = QuickOffset.NO;
result.setDistinct();
}
} else if (distinctExpressions != null) {
- quickOffset = false;
+ quickOffset = QuickOffset.NO;
result = createLocalResult(result);
result.setDistinct(distinctIndexes);
}
@@ -813,25 +872,25 @@ protected ResultInterface queryWithoutCache(long maxRows, ResultTarget target) {
// Cannot apply limit now if percent is specified
long limit = fetchPercent ? -1 : fetch;
if (isQuickAggregateQuery) {
- queryQuick(columnCount, to, quickOffset && offset > 0);
+ queryQuick(columnCount, to, quickOffset == QuickOffset.YES && offset > 0);
} else if (isWindowQuery) {
if (isGroupQuery) {
- queryGroupWindow(columnCount, result, offset, quickOffset);
+ queryGroupWindow(columnCount, result, offset, quickOffset == QuickOffset.YES);
} else {
- queryWindow(columnCount, result, offset, quickOffset);
+ queryWindow(columnCount, result, offset, quickOffset == QuickOffset.YES);
}
} else if (isGroupQuery) {
if (isGroupSortedQuery) {
- lazyResult = queryGroupSorted(columnCount, to, offset, quickOffset);
+ lazyResult = queryGroupSorted(columnCount, to, offset, quickOffset == QuickOffset.YES);
} else {
- queryGroup(columnCount, result, offset, quickOffset);
+ queryGroup(columnCount, result, offset, quickOffset == QuickOffset.YES);
}
} else if (isDistinctQuery) {
- queryDistinct(to, offset, limit, withTies, quickOffset);
+ queryDistinct(to, offset, limit, withTies, quickOffset == QuickOffset.YES);
} else {
lazyResult = queryFlat(columnCount, to, offset, limit, withTies, quickOffset);
}
- if (quickOffset) {
+ if (quickOffset == QuickOffset.YES) {
offset = 0;
}
}
@@ -1199,62 +1258,63 @@ public void preparePlan() {
}
}
cost = preparePlan(session.isParsingCreateView());
- if (distinct && getDatabase().getSettings().optimizeDistinct &&
- !isGroupQuery && filters.size() == 1 &&
- expressions.size() == 1 && condition == null) {
+ if (distinct && getDatabase().getSettings().optimizeDistinct && !isGroupQuery && filters.size() == 1
+ && expressions.size() == 1 && condition == null) {
Expression expr = expressions.get(0);
expr = expr.getNonAliasExpression();
if (expr instanceof ExpressionColumn) {
Column column = ((ExpressionColumn) expr).getColumn();
int selectivity = column.getSelectivity();
- Index columnIndex = topTableFilter.getTable().
- getIndexForColumn(column, false, true);
- if (columnIndex != null &&
- selectivity != Constants.SELECTIVITY_DEFAULT &&
- selectivity < 20) {
+ Index columnIndex = topTableFilter.getTable().getIndexForColumn(column, false, true);
+ if (columnIndex != null && selectivity != Constants.SELECTIVITY_DEFAULT && selectivity < 20) {
Index current = topTableFilter.getIndex();
// if another index is faster
if (current == null || current.getIndexType().isScan() || columnIndex == current) {
- topTableFilter.setIndex(columnIndex);
+ topTableFilter.setIndex(columnIndex, false);
isDistinctQuery = true;
}
}
}
}
if (sort != null && !isQuickAggregateQuery && !isGroupQuery) {
- Index index = getSortIndex();
+ List sortIndexes = getIndexSorts();
Index current = topTableFilter.getIndex();
- if (index != null && current != null) {
- if (current.getIndexType().isScan() || current == index) {
- topTableFilter.setIndex(index);
- if (!topTableFilter.hasInComparisons()) {
- // in(select ...) and in(1,2,3) may return the key in
- // another order
- sortUsingIndex = true;
- }
- } else if (index.getIndexColumns() != null
- && index.getIndexColumns().length >= current
- .getIndexColumns().length) {
- IndexColumn[] sortColumns = index.getIndexColumns();
- IndexColumn[] currentColumns = current.getIndexColumns();
- boolean swapIndex = false;
- for (int i = 0; i < currentColumns.length; i++) {
- if (sortColumns[i].column != currentColumns[i].column) {
- swapIndex = false;
+ if (sortIndexes != null && current != null) {
+ loop: for (IndexSort sortIndex : sortIndexes) {
+ Index index = sortIndex.getIndex();
+ boolean reverse = sortIndex.isReverse();
+ if (current.getIndexType().isScan() || current == index) {
+ topTableFilter.setIndex(index, reverse);
+ if (!topTableFilter.hasInComparisons()) {
+ // in(select ...) and in(1,2,3) may return the key in
+ // another order
+ indexSortedColumns = sortIndex.getSortedColumns();
break;
}
- if (sortColumns[i].sortType != currentColumns[i].sortType) {
- swapIndex = true;
+ } else if (index.getIndexColumns() != null
+ && index.getIndexColumns().length >= current
+ .getIndexColumns().length) {
+ IndexColumn[] sortColumns = index.getIndexColumns();
+ IndexColumn[] currentColumns = current.getIndexColumns();
+ boolean swapIndex = false;
+ for (int i = 0; i < currentColumns.length; i++) {
+ if (sortColumns[i].column != currentColumns[i].column) {
+ continue loop;
+ }
+ if (sortColumns[i].sortType != currentColumns[i].sortType) {
+ swapIndex = true;
+ }
+ }
+ if (swapIndex) {
+ topTableFilter.setIndex(index, reverse);
+ indexSortedColumns = sortIndex.getSortedColumns();
+ break;
}
- }
- if (swapIndex) {
- topTableFilter.setIndex(index);
- sortUsingIndex = true;
}
}
}
- if (sortUsingIndex && forUpdate != null && !topTableFilter.getIndex().isRowIdIndex()) {
- sortUsingIndex = false;
+ if (indexSortedColumns > 0 && forUpdate != null && !topTableFilter.getIndex().isRowIdIndex()) {
+ indexSortedColumns = 0;
}
}
if (!isQuickAggregateQuery && isGroupQuery) {
@@ -1262,7 +1322,7 @@ public void preparePlan() {
if (index != null) {
Index current = topTableFilter.getIndex();
if (current != null && (current.getIndexType().isScan() || current == index)) {
- topTableFilter.setIndex(index);
+ topTableFilter.setIndex(index, false);
isGroupSortedQuery = true;
}
}
@@ -1448,8 +1508,11 @@ public StringBuilder getPlanSQL(StringBuilder builder, int sqlFlags) {
if (isDistinctQuery) {
builder.append("\n/* distinct */");
}
- if (sortUsingIndex) {
+ if (indexSortedColumns == IndexSort.FULLY_SORTED) {
builder.append("\n/* index sorted */");
+ } else if (indexSortedColumns > 0) {
+ builder.append("\n/* index sorted: ").append(indexSortedColumns).append(" of ") //
+ .append(sort.getOrderList().size()).append(" columns */");
}
if (isGroupQuery) {
if (isGroupSortedQuery) {
diff --git a/h2/src/main/org/h2/constraint/ConstraintReferential.java b/h2/src/main/org/h2/constraint/ConstraintReferential.java
index b35a3458ab..01be6f85aa 100644
--- a/h2/src/main/org/h2/constraint/ConstraintReferential.java
+++ b/h2/src/main/org/h2/constraint/ConstraintReferential.java
@@ -314,7 +314,7 @@ private boolean existsRow(SessionLocal session, Index searchIndex,
SearchRow check, Row excluding) {
Table searchTable = searchIndex.getTable();
searchTable.lock(session, Table.READ_LOCK);
- Cursor cursor = searchIndex.find(session, check, check);
+ Cursor cursor = searchIndex.find(session, check, check, false);
while (cursor.next()) {
SearchRow found;
found = cursor.getSearchRow();
diff --git a/h2/src/main/org/h2/engine/Database.java b/h2/src/main/org/h2/engine/Database.java
index 000fe39e8e..cbae14930b 100644
--- a/h2/src/main/org/h2/engine/Database.java
+++ b/h2/src/main/org/h2/engine/Database.java
@@ -576,7 +576,7 @@ private CreateTableData createSysTableData() {
}
private void executeMeta() {
- Cursor cursor = metaIdIndex.find(systemSession, null, null);
+ Cursor cursor = metaIdIndex.find(systemSession, null, null, false);
ArrayList firstRecords = new ArrayList<>(), domainRecords = new ArrayList<>(),
middleRecords = new ArrayList<>(), constraintRecords = new ArrayList<>(),
lastRecords = new ArrayList<>();
@@ -729,7 +729,7 @@ private void addMeta(SessionLocal session, DbObject obj) {
if (SysProperties.CHECK) {
verifyMetaLocked(session);
}
- Cursor cursor = metaIdIndex.find(session, r, r);
+ Cursor cursor = metaIdIndex.find(session, r, r, false);
if (!cursor.next()) {
meta.addRow(session, r);
} else {
@@ -838,7 +838,7 @@ public void removeMeta(SessionLocal session, int id) {
r.setValue(0, ValueInteger.get(id));
boolean wasLocked = lockMeta(session);
try {
- Cursor cursor = metaIdIndex.find(session, r, r);
+ Cursor cursor = metaIdIndex.find(session, r, r, false);
if (cursor.next()) {
Row found = cursor.get();
meta.removeRow(session, found);
@@ -1319,7 +1319,7 @@ private synchronized void closeFiles() {
private void checkMetaFree(SessionLocal session, int id) {
SearchRow r = meta.getRowFactory().createRow();
r.setValue(0, ValueInteger.get(id));
- Cursor cursor = metaIdIndex.find(session, r, r);
+ Cursor cursor = metaIdIndex.find(session, r, r, false);
if (cursor.next()) {
throw DbException.getInternalError();
}
diff --git a/h2/src/main/org/h2/expression/aggregate/Percentile.java b/h2/src/main/org/h2/expression/aggregate/Percentile.java
index 6dfc13618b..8003e76917 100644
--- a/h2/src/main/org/h2/expression/aggregate/Percentile.java
+++ b/h2/src/main/org/h2/expression/aggregate/Percentile.java
@@ -157,7 +157,7 @@ static Value getFromIndex(SessionLocal session, Expression expression, int dataT
if (count == 0) {
return ValueNull.INSTANCE;
}
- Cursor cursor = index.find(session, null, null);
+ Cursor cursor = index.find(session, null, null, false);
cursor.next();
int columnId = index.getColumns()[0].getColumnId();
ExpressionColumn expr = (ExpressionColumn) expression;
@@ -188,7 +188,7 @@ static Value getFromIndex(SessionLocal session, Expression expression, int dataT
TableFilter tableFilter = expr.getTableFilter();
SearchRow check = tableFilter.getTable().getTemplateSimpleRow(true);
check.setValue(columnId, ValueNull.INSTANCE);
- Cursor nullsCursor = index.find(session, check, check);
+ Cursor nullsCursor = index.find(session, check, check, false);
while (nullsCursor.next()) {
count--;
}
diff --git a/h2/src/main/org/h2/index/DualIndex.java b/h2/src/main/org/h2/index/DualIndex.java
index ffb90bd893..9173d6c62f 100644
--- a/h2/src/main/org/h2/index/DualIndex.java
+++ b/h2/src/main/org/h2/index/DualIndex.java
@@ -25,7 +25,7 @@ public DualIndex(DualTable table) {
}
@Override
- public Cursor find(SessionLocal session, SearchRow first, SearchRow last) {
+ public Cursor find(SessionLocal session, SearchRow first, SearchRow last, boolean reverse) {
return new DualCursor();
}
diff --git a/h2/src/main/org/h2/index/Index.java b/h2/src/main/org/h2/index/Index.java
index b6b435a5a1..c042740ab8 100644
--- a/h2/src/main/org/h2/index/Index.java
+++ b/h2/src/main/org/h2/index/Index.java
@@ -244,9 +244,10 @@ public boolean isFindUsingFullTableScan() {
* @param session the session
* @param first the first row, or null for no limit
* @param last the last row, or null for no limit
+ * @param reverse if true, iterate in reverse (descending) order
* @return the cursor to iterate over the results
*/
- public abstract Cursor find(SessionLocal session, SearchRow first, SearchRow last);
+ public abstract Cursor find(SessionLocal session, SearchRow first, SearchRow last, boolean reverse);
/**
* Estimate the cost to search for rows given the search mask.
diff --git a/h2/src/main/org/h2/index/IndexCursor.java b/h2/src/main/org/h2/index/IndexCursor.java
index 33f140187d..2c90d27d8e 100644
--- a/h2/src/main/org/h2/index/IndexCursor.java
+++ b/h2/src/main/org/h2/index/IndexCursor.java
@@ -34,6 +34,7 @@ public class IndexCursor implements Cursor {
private SessionLocal session;
private Index index;
+ private boolean reverse;
private Table table;
private IndexColumn[] indexColumns;
private boolean alwaysFalse;
@@ -52,8 +53,9 @@ public class IndexCursor implements Cursor {
public IndexCursor() {
}
- public void setIndex(Index index) {
+ public void setIndex(Index index, boolean reverse) {
this.index = index;
+ this.reverse = reverse;
this.table = index.getTable();
Column[] columns = table.getColumns();
indexColumns = new IndexColumn[columns.length];
@@ -180,10 +182,18 @@ public void find(SessionLocal s, ArrayList indexConditions) {
return;
}
if (!alwaysFalse) {
+ SearchRow first, last;
+ if (reverse) {
+ first = end;
+ last = start;
+ } else {
+ first = start;
+ last = end;
+ }
if (intersects != null && index instanceof SpatialIndex) {
- cursor = ((SpatialIndex) index).findByGeometry(session, start, end, intersects);
+ cursor = ((SpatialIndex) index).findByGeometry(session, first, last, reverse, intersects);
} else if (index != null) {
- cursor = index.find(session, start, end);
+ cursor = index.find(session, first, last, reverse);
}
}
}
@@ -384,7 +394,7 @@ private void find(Value v) {
int id = column.getColumnId();
start.setValue(id, v);
}
- cursor = index.find(session, start, start);
+ cursor = index.find(session, start, start, false);
}
@Override
diff --git a/h2/src/main/org/h2/index/IndexSort.java b/h2/src/main/org/h2/index/IndexSort.java
new file mode 100644
index 0000000000..07e3a8a3a0
--- /dev/null
+++ b/h2/src/main/org/h2/index/IndexSort.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2004-2024 H2 Group. Multiple-Licensed under the MPL 2.0,
+ * and the EPL 1.0 (https://h2database.com/html/license.html).
+ * Initial Developer: H2 Group
+ */
+package org.h2.index;
+
+/**
+ * Index-sorting information.
+ */
+public final class IndexSort implements Comparable {
+
+ /**
+ * Used instead of actual number of sorted columns when post-sorting isn't
+ * needed.
+ */
+ public static final int FULLY_SORTED = Integer.MAX_VALUE;
+
+ private final Index index;
+
+ /**
+ * A positive number of sorted columns for partial index sorts, or
+ * {@link #FULLY_SORTED} for complete index sorts.
+ */
+ private final int sortedColumns;
+
+ /**
+ * Whether index must be iterated in reverse order.
+ */
+ private final boolean reverse;
+
+ /**
+ * Creates an index sorting information for complete index sort.
+ *
+ * @param index
+ * the index
+ * @param reverse
+ * whether index must be iterated in reverse order
+ */
+ public IndexSort(Index index, boolean reverse) {
+ this(index, FULLY_SORTED, reverse);
+ }
+
+ /**
+ * Creates an index sorting information for index sort.
+ *
+ * @param index
+ * the index
+ * @param sortedColumns
+ * a positive number of sorted columns for partial index sorts,
+ * or {@link #FULLY_SORTED} for complete index sorts
+ * @param reverse
+ * whether index must be iterated in reverse order
+ */
+ public IndexSort(Index index, int sortedColumns, boolean reverse) {
+ this.index = index;
+ this.sortedColumns = sortedColumns;
+ this.reverse = reverse;
+ }
+
+ /**
+ * Returns the index.
+ *
+ * @return the index
+ */
+ public Index getIndex() {
+ return index;
+ }
+
+ /**
+ * Returns number of sorted columns.
+ *
+ * @return positive number of sorted columns for partial index sorts, or
+ * {@link #FULLY_SORTED} for complete index sorts
+ */
+ public int getSortedColumns() {
+ return sortedColumns;
+ }
+
+ /**
+ * Returns whether index must be iterated in reverse order.
+ *
+ * @return {@code true} for reverse order, {@code false} for natural order
+ */
+ public boolean isReverse() {
+ return reverse;
+ }
+
+ @Override
+ public int compareTo(IndexSort o) {
+ return o.sortedColumns - sortedColumns;
+ }
+
+}
diff --git a/h2/src/main/org/h2/index/LinkedIndex.java b/h2/src/main/org/h2/index/LinkedIndex.java
index 98dbc09366..9dc67e98bd 100644
--- a/h2/src/main/org/h2/index/LinkedIndex.java
+++ b/h2/src/main/org/h2/index/LinkedIndex.java
@@ -87,7 +87,8 @@ public void add(SessionLocal session, Row row) {
}
@Override
- public Cursor find(SessionLocal session, SearchRow first, SearchRow last) {
+ public Cursor find(SessionLocal session, SearchRow first, SearchRow last, boolean reverse) {
+ assert !reverse;
ArrayList params = Utils.newSmallArrayList();
StringBuilder builder = new StringBuilder("SELECT * FROM ").append(targetTableName).append(" T");
boolean f = false;
diff --git a/h2/src/main/org/h2/index/MetaIndex.java b/h2/src/main/org/h2/index/MetaIndex.java
index ecfaac0fa8..8da7a6c36e 100644
--- a/h2/src/main/org/h2/index/MetaIndex.java
+++ b/h2/src/main/org/h2/index/MetaIndex.java
@@ -48,7 +48,8 @@ public void remove(SessionLocal session, Row row) {
}
@Override
- public Cursor find(SessionLocal session, SearchRow first, SearchRow last) {
+ public Cursor find(SessionLocal session, SearchRow first, SearchRow last, boolean reverse) {
+ assert !reverse;
ArrayList rows = meta.generateRows(session, first, last);
return new MetaCursor(rows);
}
diff --git a/h2/src/main/org/h2/index/RangeIndex.java b/h2/src/main/org/h2/index/RangeIndex.java
index 9ae8fb5d98..3eff4d7763 100644
--- a/h2/src/main/org/h2/index/RangeIndex.java
+++ b/h2/src/main/org/h2/index/RangeIndex.java
@@ -32,7 +32,8 @@ public RangeIndex(RangeTable table, IndexColumn[] columns) {
}
@Override
- public Cursor find(SessionLocal session, SearchRow first, SearchRow last) {
+ public Cursor find(SessionLocal session, SearchRow first, SearchRow last, boolean reverse) {
+ assert !reverse;
long min = rangeTable.getMin(session);
long max = rangeTable.getMax(session);
long step = rangeTable.getStep(session);
diff --git a/h2/src/main/org/h2/index/RecursiveIndex.java b/h2/src/main/org/h2/index/RecursiveIndex.java
index 6b1a10ad03..0a2821a91c 100644
--- a/h2/src/main/org/h2/index/RecursiveIndex.java
+++ b/h2/src/main/org/h2/index/RecursiveIndex.java
@@ -61,7 +61,8 @@ public double getCost(SessionLocal session, int[] masks, TableFilter[] filters,
}
@Override
- public Cursor find(SessionLocal session, SearchRow first, SearchRow last) {
+ public Cursor find(SessionLocal session, SearchRow first, SearchRow last, boolean reverse) {
+ assert !reverse;
CTE cte = (CTE) table;
ResultInterface recursiveResult = cte.getRecursiveResult();
if (recursiveResult != null) {
diff --git a/h2/src/main/org/h2/index/RegularQueryExpressionIndex.java b/h2/src/main/org/h2/index/RegularQueryExpressionIndex.java
index d0b73d4660..fc678a2e17 100644
--- a/h2/src/main/org/h2/index/RegularQueryExpressionIndex.java
+++ b/h2/src/main/org/h2/index/RegularQueryExpressionIndex.java
@@ -158,12 +158,14 @@ public double getCost(SessionLocal session, int[] masks, TableFilter[] filters,
}
@Override
- public Cursor find(SessionLocal session, SearchRow first, SearchRow last) {
+ public Cursor find(SessionLocal session, SearchRow first, SearchRow last, boolean reverse) {
return find(session, first, last, null);
}
@Override
- public Cursor findByGeometry(SessionLocal session, SearchRow first, SearchRow last, SearchRow intersection) {
+ public Cursor findByGeometry(SessionLocal session, SearchRow first, SearchRow last, boolean reverse,
+ SearchRow intersection) {
+ assert !reverse;
return find(session, first, last, intersection);
}
diff --git a/h2/src/main/org/h2/index/SpatialIndex.java b/h2/src/main/org/h2/index/SpatialIndex.java
index e7c174c2c7..701cd02dc3 100644
--- a/h2/src/main/org/h2/index/SpatialIndex.java
+++ b/h2/src/main/org/h2/index/SpatialIndex.java
@@ -21,10 +21,12 @@ public interface SpatialIndex {
* @param session the session
* @param first the lower bound
* @param last the upper bound
+ * @param reverse if true, iterate in reverse (descending) order
* @param intersection the geometry which values should intersect with, or
* null for anything
* @return the cursor to iterate over the results
*/
- Cursor findByGeometry(SessionLocal session, SearchRow first, SearchRow last, SearchRow intersection);
+ Cursor findByGeometry(SessionLocal session, SearchRow first, SearchRow last, boolean reverse,
+ SearchRow intersection);
}
diff --git a/h2/src/main/org/h2/index/VirtualConstructedTableIndex.java b/h2/src/main/org/h2/index/VirtualConstructedTableIndex.java
index 63244c8880..8d096f9f97 100644
--- a/h2/src/main/org/h2/index/VirtualConstructedTableIndex.java
+++ b/h2/src/main/org/h2/index/VirtualConstructedTableIndex.java
@@ -34,7 +34,8 @@ public boolean isFindUsingFullTableScan() {
}
@Override
- public Cursor find(SessionLocal session, SearchRow first, SearchRow last) {
+ public Cursor find(SessionLocal session, SearchRow first, SearchRow last, boolean reverse) {
+ assert !reverse;
return new VirtualTableCursor(this, first, last, table.getResult(session));
}
diff --git a/h2/src/main/org/h2/mvstore/MVMap.java b/h2/src/main/org/h2/mvstore/MVMap.java
index a09a53679b..53ddf4644f 100644
--- a/h2/src/main/org/h2/mvstore/MVMap.java
+++ b/h2/src/main/org/h2/mvstore/MVMap.java
@@ -213,7 +213,7 @@ public final K getKey(long index) {
* @return the key list
*/
public final List keyList() {
- return new AbstractList() {
+ return new AbstractList<>() {
@Override
public K get(int index) {
@@ -733,12 +733,12 @@ public Cursor cursor(RootReference rootReference, K from, K to, boole
@Override
public final Set> entrySet() {
final RootReference rootReference = flushAndGetRoot();
- return new AbstractSet>() {
+ return new AbstractSet<>() {
@Override
public Iterator> iterator() {
final Cursor cursor = cursor(rootReference, null, null, false);
- return new Iterator>() {
+ return new Iterator<>() {
@Override
public boolean hasNext() {
@@ -771,7 +771,7 @@ public boolean contains(Object o) {
@Override
public Set keySet() {
final RootReference rootReference = flushAndGetRoot();
- return new AbstractSet() {
+ return new AbstractSet<>() {
@Override
public Iterator iterator() {
@@ -1617,7 +1617,7 @@ public abstract static class DecisionMaker {
/**
* Decision maker for transaction rollback.
*/
- public static final DecisionMaker