diff --git a/ClientSupport/ClientSupport.gradle b/ClientSupport/ClientSupport.gradle
index ec3c002ee0e..b4b426ef3c2 100644
--- a/ClientSupport/ClientSupport.gradle
+++ b/ClientSupport/ClientSupport.gradle
@@ -12,4 +12,14 @@ dependencies {
implementation project(':log-factory')
implementation project(':Configuration')
implementation depCommonsLang3
+
+ testImplementation project(':engine-test-utils')
+ Classpaths.inheritJUnitClassic(project, 'testImplementation')
+ Classpaths.inheritJUnitPlatform(project)
+ Classpaths.inheritAssertJ(project)
+
+ testRuntimeOnly project(':log-to-slf4j'),
+ project(path: ':configs'),
+ project(path: ':test-configs')
+ Classpaths.inheritSlf4j(project, 'slf4j-simple', 'testRuntimeOnly')
}
diff --git a/ClientSupport/src/main/java/io/deephaven/clientsupport/gotorow/SeekRow.java b/ClientSupport/src/main/java/io/deephaven/clientsupport/gotorow/SeekRow.java
index 5f57ff99f85..73c2a855f0c 100644
--- a/ClientSupport/src/main/java/io/deephaven/clientsupport/gotorow/SeekRow.java
+++ b/ClientSupport/src/main/java/io/deephaven/clientsupport/gotorow/SeekRow.java
@@ -3,24 +3,24 @@
//
package io.deephaven.clientsupport.gotorow;
-import java.time.Instant;
-import java.util.function.Function;
-import gnu.trove.set.TLongSet;
-import gnu.trove.set.hash.TLongHashSet;
import io.deephaven.api.util.ConcurrentMethod;
+import io.deephaven.base.verify.Assert;
import io.deephaven.base.verify.Require;
import io.deephaven.engine.rowset.RowSet;
-import io.deephaven.engine.rowset.RowSetBuilderRandom;
-import io.deephaven.engine.rowset.RowSetFactory;
import io.deephaven.engine.table.ColumnSource;
import io.deephaven.engine.table.Table;
+import io.deephaven.engine.table.impl.NotificationStepSource;
+import io.deephaven.engine.table.impl.SortedColumnsAttribute;
+import io.deephaven.engine.table.impl.SortingOrder;
+import io.deephaven.engine.table.impl.remote.ConstructSnapshot;
import io.deephaven.internal.log.LoggerFactory;
import io.deephaven.io.logger.Logger;
-import io.deephaven.time.DateTimeUtils;
+import org.apache.commons.lang3.mutable.Mutable;
+import org.apache.commons.lang3.mutable.MutableObject;
-import java.util.Random;
+import java.util.*;
-public class SeekRow implements Function
{
+public class SeekRow {
private final long startingRow;
private final String columnName;
private final Object seekValue;
@@ -28,10 +28,8 @@ public class SeekRow implements Function {
private final boolean contains;
private final boolean isBackward;
- private Comparable closestUpperValueYet;
- private Comparable closestLowerValueYet;
- private long closestUpperRowYet = -1;
- private long closestLowerRowYet = -1;
+ private ColumnSource columnSource;
+ private boolean usePrev;
private static final Logger log = LoggerFactory.getLogger(SeekRow.class);
@@ -45,155 +43,143 @@ public SeekRow(long startingRow, String columnName, Object seekValue, boolean in
this.isBackward = isBackward;
}
- @Override
@ConcurrentMethod
- public Long apply(Table table) {
- final int sortDirection = guessSorted(table);
- final boolean isSorted = !contains && sortDirection != 0;
-
- final RowSet index = table.getRowSet();
- long row;
- if (isSorted) {
- final Comparable value =
- (Comparable) table.getColumnSource(columnName).get((int) index.get((int) startingRow));
- final int compareTo =
- sortDirection * nullSafeCompare(value, (Comparable) seekValue) * (isBackward ? -1 : 1);
- final int start = isBackward ? (int) startingRow + 1 : (int) startingRow;
- if (compareTo == 0) {
- return startingRow;
- } else if (compareTo < 0) {
- // value is less than seek value
- log.info().append("Value is before: ").append(nullSafeToString(value)).append(" < ")
- .append(nullSafeToString(seekValue)).endl();
- row = maybeBinarySearch(table, index, sortDirection, start, (int) index.size() - 1);
- } else {
- log.info().append("Value is after: ").append(nullSafeToString(value)).append(" > ")
- .append(nullSafeToString(seekValue)).endl();
- row = maybeBinarySearch(table, index, sortDirection, 0, start);
- }
- if (row >= 0) {
- return row;
- }
- // we aren't really sorted
- }
+ public long seek(Table table) {
+ final Mutable result = new MutableObject<>(null);
+
+ ConstructSnapshot.callDataSnapshotFunction("SeekRow",
+ ConstructSnapshot.makeSnapshotControl(false, table.isRefreshing(), (NotificationStepSource) table),
+ ((nestedUsePrev, beforeClockValue) -> {
+ final Optional order = SortedColumnsAttribute.getOrderForColumn(table, columnName);
+ final RowSet rowSet = table.getRowSet();
+ columnSource = table.getColumnSource(columnName);
+ usePrev = nestedUsePrev;
+
+ if (order.isPresent()) {
+ final Comparable currValue = (Comparable) columnSourceGet(rowSet.get(startingRow));
+ int compareResult = nullSafeCompare(currValue, (Comparable) seekValue);
+
+ if (isBackward) {
+ // current row is seek value, check prev row
+ if (compareResult == 0 && startingRow > 0) {
+ final Comparable prevValue = (Comparable) columnSourceGet(rowSet.get(startingRow - 1));
+ if (nullSafeCompare(prevValue, (Comparable) seekValue) == 0) {
+ result.setValue(startingRow - 1);
+ return true;
+ }
+ // prev row is not the seek value, loop to back and find the last value
+ // algorithm is the same as if seek value is below the current row
+ } else if ((compareResult > 0 && order.get() == SortingOrder.Ascending)
+ || (compareResult < 0 && order.get() == SortingOrder.Descending)) {
+ // current row is greater than seek value and ascending
+ // current row is less than seek value and descending
+ // which means seek value is above the current row, find the last occurrence
+ result.setValue(findEdgeOccurrence(rowSet, 0, startingRow, false,
+ order.get() == SortingOrder.Ascending));
+ return true;
+ }
+ // seek value is below the current row
+ // loop to back and find the last value
+ result.setValue(findEdgeOccurrence(rowSet, startingRow, rowSet.size() - 1, false,
+ order.get() == SortingOrder.Ascending));
+ return true;
+
+ } else {
+ // current row is seek value, check next row
+ if (compareResult == 0 && startingRow < rowSet.size() - 1) {
+ final Comparable nextValue = (Comparable) columnSourceGet(rowSet.get(startingRow + 1));
+ if (nullSafeCompare(nextValue, (Comparable) seekValue) == 0) {
+ result.setValue(startingRow + 1);
+ return true;
+ }
+ // next row is not the seek value, loop to start and find the first value
+ // algorithm is the same as if seek value is above the current row
+ } else if ((compareResult < 0 && order.get() == SortingOrder.Ascending)
+ || (compareResult > 0 && order.get() == SortingOrder.Descending)) {
+ // current row is less than seek value and ascending
+ // current row is greater than seek value and descending
+ // which means seek value is below the current row, find the first occurrence
+ result.setValue(
+ findEdgeOccurrence(rowSet, startingRow, rowSet.size() - 1, true,
+ order.get() == SortingOrder.Ascending));
+ return true;
+ }
+ // seek value is above the current row
+ // loop to start and find the first value
+ result.setValue(findEdgeOccurrence(rowSet, 0, startingRow, true,
+ order.get() == SortingOrder.Ascending));
+ return true;
+ }
+ }
- if (isBackward) {
- row = findRow(table, index, 0, (int) startingRow);
- if (row >= 0) {
- return row;
- }
- row = findRow(table, index, (int) startingRow, (int) index.size());
- if (row >= 0) {
- return row;
- }
- } else {
- row = findRow(table, index, (int) startingRow + 1, (int) index.size());
- if (row >= 0) {
- return row;
- }
- row = findRow(table, index, 0, (int) startingRow + 1);
- if (row >= 0) {
- return row;
- }
- }
+ long row;
+ if (isBackward) {
+ row = findRow(rowSet, 0, (int) startingRow);
+ if (row >= 0) {
+ result.setValue(row);
+ return true;
+ }
+ row = findRow(rowSet, (int) startingRow, (int) rowSet.size());
+ if (row >= 0) {
+ result.setValue(row);
+ return true;
+ }
+ } else {
+ row = findRow(rowSet, (int) startingRow + 1, (int) rowSet.size());
+ if (row >= 0) {
+ result.setValue(row);
+ return true;
+ }
+ row = findRow(rowSet, 0, (int) startingRow + 1);
+ if (row >= 0) {
+ result.setValue(row);
+ return true;
+ }
+ }
+ result.setValue(-1L);
+ return true;
+ }));
- // just go to the closest value
- if (closestLowerValueYet == null && closestUpperValueYet == null) {
- return -1L;
- } else if (closestLowerValueYet == null) {
- return index.find(closestUpperRowYet);
- } else if (closestUpperValueYet == null) {
- return index.find(closestLowerRowYet);
- } else {
- // we need to decide between the two
- Class columnType = table.getColumnSource(columnName).getType();
- if (Number.class.isAssignableFrom(columnType)) {
- double nu = ((Number) closestUpperValueYet).doubleValue();
- double nl = ((Number) closestLowerRowYet).doubleValue();
- double ns = ((Number) seekValue).doubleValue();
- double du = Math.abs(nu - ns);
- double dl = Math.abs(nl - ns);
- log.info().append("Using numerical distance (").appendDouble(dl).append(", ").appendDouble(du)
- .append(")").endl();
- return index.find(du < dl ? closestUpperRowYet : closestLowerRowYet);
- } else if (Instant.class.isAssignableFrom(columnType)) {
- long nu = DateTimeUtils.epochNanos(((Instant) closestUpperValueYet));
- long nl = DateTimeUtils.epochNanos(((Instant) closestLowerValueYet));
- long ns = DateTimeUtils.epochNanos(((Instant) seekValue));
- long du = Math.abs(nu - ns);
- long dl = Math.abs(nl - ns);
- log.info().append("Using nano distance (").append(dl).append(", ").append(du).append(")").endl();
- return index.find(du < dl ? closestUpperRowYet : closestLowerRowYet);
- } else {
- long nu = index.find(closestUpperRowYet);
- long nl = index.find(closestLowerRowYet);
- long ns = startingRow;
- long du = Math.abs(nu - ns);
- long dl = Math.abs(nl - ns);
- log.info().append("Using index distance (").append(dl).append(", ").append(du).append(")").endl();
- return du < dl ? nu : nl;
- }
- }
+ Assert.neqNull(result.getValue(), "result.getValue()");
+ return result.getValue();
}
- private long maybeBinarySearch(Table table, RowSet index, int sortDirection, int start, int end) {
- log.info().append("Doing binary search ").append(start).append(", ").append(end).endl();
-
- final ColumnSource columnSource = table.getColumnSource(columnName);
-
- int minBound = start;
- int maxBound = end;
-
- Comparable minValue = (Comparable) columnSource.get(index.get(minBound));
- Comparable maxValue = (Comparable) columnSource.get(index.get(maxBound));
-
- final Comparable comparableSeek = (Comparable) this.seekValue;
-
- log.info().append("Seek Value ").append(nullSafeToString(comparableSeek)).endl();
-
- if (nullSafeCompare(minValue, comparableSeek) * sortDirection >= 0) {
- log.info().append("Less than min ").append(nullSafeToString(comparableSeek)).append(" < ")
- .append(nullSafeToString(minValue)).endl();
- return minBound;
- } else if (nullSafeCompare(maxValue, comparableSeek) * sortDirection <= 0) {
- log.info().append("Greater than max: ").append(nullSafeToString(comparableSeek)).append(" < ")
- .append(nullSafeToString(maxValue)).endl();
- return maxBound;
- }
-
-
- do {
- log.info().append("Bounds (").append(minBound).append(", ").append(maxBound).append(")").endl();
- if (minBound == maxBound || minBound == maxBound - 1) {
- return minBound;
- }
-
- if (nullSafeCompare(minValue, maxValue) * sortDirection > 0) {
- log.info().append("Not Sorted (").append(minValue.toString()).append(", ").append(maxValue.toString())
- .append(")").endl();
- // not really sorted
- return -1;
- }
-
- final int check = (minBound + maxBound) / 2;
- final Comparable checkValue = (Comparable) columnSource.get(index.get(check));
- // Search up by default, reverse the result to search down
- final int compareResult =
- nullSafeCompare(checkValue, comparableSeek) * sortDirection * (isBackward ? -1 : 1);
+ /**
+ * Finds the first/last occurrence of the target value by using binary search
+ *
+ * @param start the starting index to search
+ * @param end the ending index to search
+ * @param findFirst whether to find the first or last occurrence (false for last)
+ * @param isAscending whether the table is sorted in ascending order (false for descending)
+ * @return the index of the first/last occurrence of the target value, -1 if not found
+ */
+ private long findEdgeOccurrence(RowSet index, long start, long end, boolean findFirst,
+ boolean isAscending) {
+ long result = -1;
- log.info().append("Check[").append(check).append("] ").append(checkValue.toString()).append(" -> ")
- .append(compareResult).endl();
+ while (start <= end) {
+ long mid = start + (end - start) / 2;
+ Comparable midValue = (Comparable) columnSourceGet((int) index.get((int) mid));
+ int compareResult = nullSafeCompare(midValue, (Comparable) seekValue);
if (compareResult == 0) {
- return check;
- } else if (compareResult < 0) {
- minBound = check;
- minValue = checkValue;
+ result = mid;
+ if (findFirst) {
+ end = mid - 1;
+ } else {
+ start = mid + 1;
+ }
+ } else if ((compareResult < 0 && isAscending) || (compareResult > 0 && !isAscending)) {
+ // mid less than target and list is ascending
+ // mid more than target and list is descending
+ // search right half
+ start = mid + 1;
} else {
- maxBound = check;
- maxValue = checkValue;
+ // other way around, search left half
+ end = mid - 1;
}
- } while (true);
+ }
+ return result;
}
int nullSafeCompare(Comparable c1, Comparable c2) {
@@ -213,11 +199,11 @@ int nullSafeCompare(Comparable c1, Comparable c2) {
return c1.compareTo(c2);
}
- String nullSafeToString(Object o) {
- return o == null ? "(null)" : o.toString();
+ private Object columnSourceGet(long rowKey) {
+ return usePrev ? columnSource.getPrev(rowKey) : columnSource.get(rowKey);
}
- private long findRow(Table table, RowSet index, int start, int end) {
+ private long findRow(RowSet index, int start, int end) {
final RowSet subIndex = index.subSetByPositionRange(start, end);
final RowSet.Iterator it;
@@ -227,17 +213,12 @@ private long findRow(Table table, RowSet index, int start, int end) {
it = subIndex.iterator();
}
- final ColumnSource columnSource = table.getColumnSource(columnName);
-
- final boolean isComparable = !contains
- && (Comparable.class.isAssignableFrom(columnSource.getType()) || columnSource.getType().isPrimitive());
-
final Object useSeek =
(seekValue instanceof String && insensitive) ? ((String) seekValue).toLowerCase() : seekValue;
for (; it.hasNext();) {
long key = it.nextLong();
- Object value = columnSource.get(key);
+ Object value = columnSourceGet(key);
if (useSeek instanceof String) {
value = value == null ? null : value.toString();
if (insensitive) {
@@ -246,111 +227,13 @@ private long findRow(Table table, RowSet index, int start, int end) {
}
// noinspection ConstantConditions
if (contains && value != null && ((String) value).contains((String) useSeek)) {
- return (long) Require.geqZero(index.find(key), "index.find(key)");
+ return Require.geqZero(index.find(key), "index.find(key)");
}
if (value == useSeek || (useSeek != null && useSeek.equals(value))) {
- return (long) Require.geqZero(index.find(key), "index.find(key)");
- }
-
- if (isComparable && useSeek != null && value != null) {
- // noinspection unchecked
- long compareResult = ((Comparable) useSeek).compareTo(value);
- if (compareResult < 0) {
- // seekValue is less than value
- if (closestUpperRowYet == -1) {
- closestUpperRowYet = key;
- closestUpperValueYet = (Comparable) value;
- } else {
- // noinspection unchecked
- if (closestUpperValueYet.compareTo(value) > 0) {
- closestUpperValueYet = (Comparable) value;
- closestUpperRowYet = key;
- }
- }
- } else {
- // seekValue is greater than value
- // seekValue is less than value
- if (closestLowerRowYet == -1) {
- closestLowerRowYet = key;
- closestLowerValueYet = (Comparable) value;
- } else {
- // noinspection unchecked
- if (closestLowerValueYet.compareTo(value) < 0) {
- closestLowerValueYet = (Comparable) value;
- closestLowerRowYet = key;
- }
- }
- }
+ return Require.geqZero(index.find(key), "index.find(key)");
}
}
return -1L;
}
-
- /**
- * Take a guess as to whether the table is sorted, such that we should do a binary search instead
- *
- * @param table the table to check for sorted-ness
- * @return 0 if the table is not sorted; 1 if might be ascending sorted, -1 if it might be descending sorted.
- */
- int guessSorted(Table table) {
- final ColumnSource columnSource = table.getColumnSource(columnName);
- if (!Comparable.class.isAssignableFrom(columnSource.getType())) {
- return 0;
- }
-
- RowSet index = table.getRowSet();
- if (index.size() > 10000) {
- Random random = new Random();
- TLongSet set = new TLongHashSet();
- long sampleSize = Math.min(index.size() / 4, 10000L);
- while (sampleSize > 0) {
- final long row = (long) (random.nextDouble() * index.size() - 1);
- if (set.add(row)) {
- sampleSize--;
- }
- }
- RowSetBuilderRandom builder = RowSetFactory.builderRandom();
- set.forEach(row -> {
- builder.addKey(table.getRowSet().get(row));
- return true;
- });
- index = builder.build();
- }
-
- boolean isAscending = true;
- boolean isDescending = true;
- boolean first = true;
-
- Object previous = null;
- for (RowSet.Iterator it = index.iterator(); it.hasNext();) {
- long key = it.nextLong();
- Object current = columnSource.get(key);
- if (current == previous) {
- continue;
- }
-
- int compareTo = first ? 0 : nullSafeCompare((Comparable) previous, (Comparable) current);
- first = false;
-
- if (compareTo > 0) {
- isAscending = false;
- } else if (compareTo < 0) {
- isDescending = false;
- }
-
- if (!isAscending && !isDescending) {
- break;
- }
-
- previous = current;
- }
-
- if (isAscending)
- return 1;
- else if (isDescending)
- return -1;
- else
- return 0;
- }
}
diff --git a/ClientSupport/src/test/java/io/deephaven/clientsupport/gotorow/SeekRowTest.java b/ClientSupport/src/test/java/io/deephaven/clientsupport/gotorow/SeekRowTest.java
new file mode 100644
index 00000000000..d2367a00f3c
--- /dev/null
+++ b/ClientSupport/src/test/java/io/deephaven/clientsupport/gotorow/SeekRowTest.java
@@ -0,0 +1,313 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.clientsupport.gotorow;
+
+import io.deephaven.engine.table.Table;
+import io.deephaven.engine.testutil.junit4.EngineCleanup;
+import io.deephaven.engine.util.TableTools;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static io.deephaven.engine.util.TableTools.intCol;
+import static io.deephaven.engine.util.TableTools.newTable;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+
+public class SeekRowTest {
+
+ @Rule
+ public final EngineCleanup framework = new EngineCleanup();
+
+ /**
+ * Helper to verify that a given value can not be found no matter which row is started from
+ *
+ * @param t the table to search
+ * @param impossibleValue a value that isn't present in the table
+ */
+ public static void assertNotFound(int impossibleValue, Table t) {
+ // ensure we can't find values that don't exist
+ for (int i = 0; i < t.size(); i++) {
+ assertSeekPosition(t, impossibleValue, true, i, -1);
+ assertSeekPosition(t, impossibleValue, false, i, -1);
+ }
+ }
+
+ /**
+ * Helper to run SeekRow and validate that the discovered row is what was expected. Validates the
+ * {@code expectedPosition} before running, to ensure test data makes sense.
+ *
+ * @param t the table to search
+ * @param seekValue the value to search for
+ * @param seekForward true to seek forward, false to seek backward
+ * @param currentPosition the position to start searching
+ * @param expectedPosition the next expected position of the seek value
+ */
+ private static void assertSeekPosition(Table t, int seekValue, boolean seekForward, int currentPosition,
+ int expectedPosition) {
+ if (expectedPosition != -1) {
+ // Confirm that the expected position matches
+ assertEquals(seekValue, t.flatten().getColumnSource("num").getInt(expectedPosition));
+ } else {
+ // Confirm that the value actually doesn't exist
+ assertTrue(t.where("num" + "=" + seekValue).isEmpty());
+ }
+ // Actually perform the requested assertion
+ SeekRow seek = new SeekRow(currentPosition, "num", seekValue, false, false, !seekForward);
+ assertEquals(expectedPosition, seek.seek(t));
+ }
+
+ /**
+ * Helper to seek from every row in a table, and assert that a valid value can be found in a valid position from
+ * each.
+ *
+ * @param t the table to search
+ * @param seekValue the value to search for
+ * @param forwardPositions expected positions when searching forward, indexed on starting row
+ * @param backwardPositions expected positions when searching backwards, indexed on starting row
+ */
+ private static void assertSeekPositionAllRows(Table t, int seekValue, int[] forwardPositions,
+ int[] backwardPositions) {
+ assertEquals(t.size(), forwardPositions.length);
+ assertEquals(t.size(), backwardPositions.length);
+ for (int i = 0; i < t.size(); i++) {
+ // seek from the current position, confirm we get the expected position
+ assertSeekPosition(t, seekValue, true, i, forwardPositions[i]);
+ assertSeekPosition(t, seekValue, false, i, backwardPositions[i]);
+ }
+ }
+
+ /**
+ * Helper to run asserts for int rows that are already sorted at initialization
+ *
+ * @param data the data, must be sorted in ascending order already
+ * @param seekValue the value to search for
+ * @param ascForwardPositions expected positions when searching forwards for ascending data
+ * @param ascBackwardPositions expected positions when searching backwards for ascending data
+ * @param descForwardPositions expected positions when searching forwards for descending data
+ * @param descBackwardPositions expected positions when searching backwards for descending data
+ */
+ private static void assertNaturallySorted(int[] data, int seekValue,
+ int[] ascForwardPositions, int[] ascBackwardPositions,
+ int[] descForwardPositions, int[] descBackwardPositions) {
+ // ascending tables
+ Table ascUnsorted = TableTools.newTable(intCol("num", data));
+ Table ascSorted = ascUnsorted.sort("num");
+ // reverse data to be in descending
+ for (int i = 0; i < data.length / 2; i++) {
+ int tmp = data[i];
+ data[i] = data[data.length - i - 1];
+ data[data.length - i - 1] = tmp;
+ }
+ // descending tables
+ Table descUnsorted = TableTools.newTable(intCol("num", data));
+ Table descSorted = descUnsorted.sortDescending("num");
+
+ assertSeekPositionAllRows(ascUnsorted, seekValue, ascForwardPositions, ascBackwardPositions);
+ assertSeekPositionAllRows(ascSorted, seekValue, ascForwardPositions, ascBackwardPositions);
+ assertSeekPositionAllRows(descUnsorted, seekValue, descForwardPositions, descBackwardPositions);
+ assertSeekPositionAllRows(descSorted, seekValue, descForwardPositions, descBackwardPositions);
+ }
+
+ @Test
+ public void emptyTable() {
+ Table t = TableTools.newTable(intCol("num"));
+
+ assertSeekPosition(t, 1, true, 0, -1);
+ assertSeekPosition(t, 1, false, 0, -1);
+
+ // repeat with sorted
+ t = t.sort("num");
+ assertSeekPosition(t, 1, true, 0, -1);
+ assertSeekPosition(t, 1, false, 0, -1);
+ }
+
+ @Test
+ public void singleRow() {
+ Table t = TableTools.newTable(intCol("num", 1));
+ assertSeekPosition(t, 1, true, 0, 0);
+ assertSeekPosition(t, 1, false, 0, 0);
+
+ assertSeekPosition(t, 100, false, 0, -1);
+ assertSeekPosition(t, 100, true, 0, -1);
+
+ // repeat with sorted
+ t = t.sort("num");
+ assertSeekPosition(t, 1, true, 0, 0);
+ assertSeekPosition(t, 1, false, 0, 0);
+
+ assertSeekPosition(t, 100, false, 0, -1);
+ assertSeekPosition(t, 100, true, 0, -1);
+
+ // repeat with sorted descending
+ t = t.sortDescending("num");
+ assertSeekPosition(t, 1, true, 0, 0);
+ assertSeekPosition(t, 1, false, 0, 0);
+
+ assertSeekPosition(t, 100, false, 0, -1);
+ assertSeekPosition(t, 100, true, 0, -1);
+ }
+
+ @Test
+ public void mono1() {
+ assertNaturallySorted(
+ new int[] {1}, 1,
+ new int[] {0},
+ new int[] {0},
+ new int[] {0},
+ new int[] {0});
+ }
+
+ @Test
+ public void mono2() {
+ assertNaturallySorted(
+ new int[] {1, 1}, 1,
+ new int[] {1, 0},
+ new int[] {1, 0},
+ new int[] {1, 0},
+ new int[] {1, 0});
+ }
+
+ @Test
+ public void mono3() {
+ assertNaturallySorted(
+ new int[] {1, 1, 1}, 1,
+ new int[] {1, 2, 0},
+ new int[] {2, 0, 1},
+ new int[] {1, 2, 0},
+ new int[] {2, 0, 1});
+ }
+
+ @Test
+ public void start1() {
+ assertNaturallySorted(
+ new int[] {1, 2}, 1,
+ new int[] {0, 0},
+ new int[] {0, 0},
+ new int[] {1, 1},
+ new int[] {1, 1});
+ }
+
+ @Test
+ public void start2() {
+ assertNaturallySorted(
+ new int[] {1, 1, 2}, 1,
+ new int[] {1, 0, 0},
+ new int[] {1, 0, 1},
+ new int[] {1, 2, 1},
+ new int[] {2, 2, 1});
+ }
+
+ @Test
+ public void start3() {
+ assertNaturallySorted(
+ new int[] {1, 1, 1, 2}, 1,
+ new int[] {1, 2, 0, 0},
+ new int[] {2, 0, 1, 2},
+ new int[] {1, 2, 3, 1},
+ new int[] {3, 3, 1, 2});
+ }
+
+ @Test
+ public void middle1() {
+ assertNaturallySorted(
+ new int[] {1, 2, 3}, 2,
+ new int[] {1, 1, 1},
+ new int[] {1, 1, 1},
+ new int[] {1, 1, 1},
+ new int[] {1, 1, 1});
+ }
+
+ @Test
+ public void middle2() {
+ assertNaturallySorted(
+ new int[] {1, 2, 2, 3}, 2,
+ new int[] {1, 2, 1, 1},
+ new int[] {2, 2, 1, 2},
+ new int[] {1, 2, 1, 1},
+ new int[] {2, 2, 1, 2});
+ }
+
+ @Test
+ public void middle3() {
+ assertNaturallySorted(
+ new int[] {1, 2, 2, 2, 3}, 2,
+ new int[] {1, 2, 3, 1, 1},
+ new int[] {3, 3, 1, 2, 3},
+ new int[] {1, 2, 3, 1, 1},
+ new int[] {3, 3, 1, 2, 3});
+ }
+
+ @Test
+ public void end1() {
+ assertNaturallySorted(
+ new int[] {1, 2}, 2,
+ new int[] {1, 1},
+ new int[] {1, 1},
+ new int[] {0, 0},
+ new int[] {0, 0});
+ }
+
+ @Test
+ public void end2() {
+ assertNaturallySorted(
+ new int[] {1, 2, 2}, 2,
+ new int[] {1, 2, 1},
+ new int[] {2, 2, 1},
+ new int[] {1, 0, 0},
+ new int[] {1, 0, 1});
+ }
+
+ @Test
+ public void end3() {
+ assertNaturallySorted(
+ new int[] {1, 2, 2, 2}, 2,
+ new int[] {1, 2, 3, 1},
+ new int[] {3, 3, 1, 2},
+ new int[] {1, 2, 0, 0},
+ new int[] {2, 0, 1, 2});
+ }
+
+ @Test
+ public void notFound() {
+ assertNaturallySorted(
+ new int[] {2, 4, 6}, 1,
+ new int[] {-1, -1, -1},
+ new int[] {-1, -1, -1},
+ new int[] {-1, -1, -1},
+ new int[] {-1, -1, -1});
+ assertNaturallySorted(
+ new int[] {2, 4, 6}, 3,
+ new int[] {-1, -1, -1},
+ new int[] {-1, -1, -1},
+ new int[] {-1, -1, -1},
+ new int[] {-1, -1, -1});
+ assertNaturallySorted(
+ new int[] {2, 4, 6}, 5,
+ new int[] {-1, -1, -1},
+ new int[] {-1, -1, -1},
+ new int[] {-1, -1, -1},
+ new int[] {-1, -1, -1});
+ assertNaturallySorted(
+ new int[] {2, 4, 6}, 7,
+ new int[] {-1, -1, -1},
+ new int[] {-1, -1, -1},
+ new int[] {-1, -1, -1},
+ new int[] {-1, -1, -1});
+ }
+
+ @Test
+ public void unsorted() {
+ final Table t = newTable(intCol("num", 3, 1, 1, 2, 3, 1, 1, 2));
+ assertSeekPositionAllRows(t, 1,
+ new int[] {1, 2, 5, 5, 5, 6, 1, 1},
+ new int[] {6, 6, 1, 2, 2, 2, 5, 6});
+ assertSeekPositionAllRows(t, 2,
+ new int[] {3, 3, 3, 7, 7, 7, 7, 3},
+ new int[] {7, 7, 7, 7, 3, 3, 3, 3});
+ assertSeekPositionAllRows(t, 3,
+ new int[] {4, 4, 4, 4, 0, 0, 0, 0},
+ new int[] {4, 0, 0, 0, 0, 4, 4, 4});
+ }
+}
diff --git a/server/src/main/java/io/deephaven/server/table/ops/TableServiceGrpcImpl.java b/server/src/main/java/io/deephaven/server/table/ops/TableServiceGrpcImpl.java
index e67eba02bf8..8d5de5f9df4 100644
--- a/server/src/main/java/io/deephaven/server/table/ops/TableServiceGrpcImpl.java
+++ b/server/src/main/java/io/deephaven/server/table/ops/TableServiceGrpcImpl.java
@@ -459,13 +459,13 @@ public void seekRow(
final String columnName = request.getColumnName();
final Class> dataType = table.getDefinition().getColumn(columnName).getDataType();
final Object seekValue = getSeekValue(request.getSeekValue(), dataType);
- final Long result = table.apply(new SeekRow(
+ final long result = new SeekRow(
request.getStartingRow(),
columnName,
seekValue,
request.getInsensitive(),
request.getContains(),
- request.getIsBackward()));
+ request.getIsBackward()).seek(table);
SeekRowResponse.Builder rowResponse = SeekRowResponse.newBuilder();
safelyComplete(responseObserver, rowResponse.setResultRow(result).build());
});