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()); });