From eb94edab0d956c6164d167a799eeebbe59f338ea Mon Sep 17 00:00:00 2001
From: Nathaniel Bauernfeind <natebauernfeind@deephaven.io>
Date: Thu, 19 Jan 2023 08:34:50 -0700
Subject: [PATCH 1/5] Avoid Redirection of Row Agnostic Column Sources

Port DH-12201: Create BooleanSingleValueSource
Port DH-12310: SingleValue Column Sources Must Handle NULL_ROW_KEY
---
 .../deephaven/engine/rowset/RowSequence.java  |   1 -
 .../engine/table/impl/AsOfJoinHelper.java     |   3 +-
 .../engine/table/impl/CrossJoinHelper.java    |  12 +-
 .../engine/table/impl/FlattenOperation.java   |   2 +-
 .../engine/table/impl/NaturalJoinHelper.java  |   3 +-
 .../engine/table/impl/QueryTable.java         |   2 +-
 .../engine/table/impl/SortOperation.java      |  23 ++-
 ...BaseAddOnlyFirstOrLastChunkedOperator.java |   5 +-
 .../impl/by/FirstOrLastChunkedOperator.java   |  11 +-
 ...regationStateManagerOpenAddressedBase.java |   3 +-
 ...ratorAggregationStateManagerTypedBase.java |   2 +-
 .../by/SortedFirstOrLastChunkedOperator.java  |   5 +-
 ...regationStateManagerOpenAddressedBase.java |   2 +-
 ...ratorAggregationStateManagerTypedBase.java |   2 +-
 .../impl/remote/InitialSnapshotTable.java     |   2 +-
 .../impl/replay/QueryReplayGroupedTable.java  |   2 +-
 .../analyzers/SelectAndViewAnalyzer.java      |   5 +-
 .../select/analyzers/StaticFlattenLayer.java  |   2 +-
 .../impl/sources/BitMaskingColumnSource.java  |  17 +-
 .../impl/sources/BitShiftingColumnSource.java |  19 ++-
 .../sources/BooleanSingleValueSource.java     | 149 ++++++++++++++++++
 .../impl/sources/ByteSingleValueSource.java   |  55 ++++++-
 .../sources/CharacterSingleValueSource.java   |  55 ++++++-
 .../sources/CrossJoinRightColumnSource.java   |  30 +++-
 .../impl/sources/DoubleSingleValueSource.java |  55 ++++++-
 .../impl/sources/FloatSingleValueSource.java  |  55 ++++++-
 .../sources/IntegerSingleValueSource.java     |  55 ++++++-
 .../impl/sources/LongSingleValueSource.java   |  55 ++++++-
 .../impl/sources/NullValueColumnSource.java   |  23 ++-
 .../impl/sources/ObjectSingleValueSource.java |  55 ++++++-
 .../impl/sources/RedirectedColumnSource.java  |  19 ++-
 .../sources/RowKeyAgnosticColumnSource.java   |  16 ++
 .../impl/sources/ShortSingleValueSource.java  |  55 ++++++-
 .../impl/sources/SingleValueColumnSource.java |  13 +-
 .../WritableRedirectedColumnSource.java       |  27 +++-
 .../ImmutableConstantByteSource.java          |  31 +++-
 .../ImmutableConstantCharSource.java          |  31 +++-
 .../ImmutableConstantDoubleSource.java        |  31 +++-
 .../ImmutableConstantFloatSource.java         |  31 +++-
 .../immutable/ImmutableConstantIntSource.java |  31 +++-
 .../ImmutableConstantLongSource.java          |  31 +++-
 .../ImmutableConstantObjectSource.java        |  31 +++-
 .../ImmutableConstantShortSource.java         |  31 +++-
 .../internal/BaseByteUpdateByOperator.java    |   2 +-
 .../internal/BaseCharUpdateByOperator.java    |   2 +-
 .../internal/BaseDoubleUpdateByOperator.java  |   2 +-
 .../internal/BaseFloatUpdateByOperator.java   |   2 +-
 .../internal/BaseIntUpdateByOperator.java     |   2 +-
 .../internal/BaseLongUpdateByOperator.java    |   2 +-
 .../internal/BaseObjectUpdateByOperator.java  |   2 +-
 .../internal/BaseShortUpdateByOperator.java   |   2 +-
 .../impl/util/ColumnsToRowsTransform.java     |   2 +-
 .../table/impl/QueryTableNaturalJoinTest.java |  22 +++
 .../impl/StreamTableAggregationTest.java      |   2 +-
 .../table/impl/StreamTableOperationsTest.java |   2 +-
 .../barrage/table/BarrageTable.java           |   2 +-
 .../ReplicateSourcesAndChunks.java            |   7 +-
 57 files changed, 1049 insertions(+), 92 deletions(-)
 create mode 100644 engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BooleanSingleValueSource.java
 create mode 100644 engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RowKeyAgnosticColumnSource.java

diff --git a/engine/rowset/src/main/java/io/deephaven/engine/rowset/RowSequence.java b/engine/rowset/src/main/java/io/deephaven/engine/rowset/RowSequence.java
index 5e9611e499f..a8d5e416e0e 100644
--- a/engine/rowset/src/main/java/io/deephaven/engine/rowset/RowSequence.java
+++ b/engine/rowset/src/main/java/io/deephaven/engine/rowset/RowSequence.java
@@ -8,7 +8,6 @@
 import io.deephaven.util.datastructures.LongSizedDataStructure;
 import io.deephaven.engine.rowset.chunkattributes.OrderedRowKeyRanges;
 import io.deephaven.engine.rowset.chunkattributes.OrderedRowKeys;
-import io.deephaven.engine.rowset.chunkattributes.RowKeys;
 import io.deephaven.chunk.LongChunk;
 import io.deephaven.chunk.WritableLongChunk;
 import io.deephaven.util.datastructures.LongRangeAbortableConsumer;
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/AsOfJoinHelper.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/AsOfJoinHelper.java
index 66800ca77c3..24125ecfdf6 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/AsOfJoinHelper.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/AsOfJoinHelper.java
@@ -1528,7 +1528,8 @@ private static QueryTable makeResult(QueryTable leftTable, Table rightTable, Row
             MatchPair[] columnsToAdd, boolean refreshing) {
         final Map<String, ColumnSource<?>> columnSources = new LinkedHashMap<>(leftTable.getColumnSourceMap());
         Arrays.stream(columnsToAdd).forEach(mp -> {
-            final RedirectedColumnSource<?> rightSource =
+            // note that we must always redirect the right-hand side, because unmatched rows will be redirected to null
+            final ColumnSource<?> rightSource =
                     new RedirectedColumnSource<>(rowRedirection, rightTable.getColumnSource(mp.rightColumn()));
             if (refreshing) {
                 rightSource.startTrackingPrevValues();
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/CrossJoinHelper.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/CrossJoinHelper.java
index 972f81a0ec3..7f188cbcf22 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/CrossJoinHelper.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/CrossJoinHelper.java
@@ -177,7 +177,8 @@ private static QueryTable internalJoin(
 
                     return makeResult(leftTable, rightTable, columnsToAdd, resultStateManager,
                             resultRowSet.toTracking(),
-                            cs -> new CrossJoinRightColumnSource<>(resultStateManager, cs, rightTable.isRefreshing()));
+                            cs -> CrossJoinRightColumnSource.maybeWrap(
+                                    resultStateManager, cs, rightTable.isRefreshing()));
                 }
 
                 final LeftOnlyIncrementalChunkedCrossJoinStateManager jsm =
@@ -190,7 +191,7 @@ private static QueryTable internalJoin(
                 final TrackingWritableRowSet resultRowSet =
                         jsm.buildLeftTicking(leftTable, rightTable, bucketingContext.rightSources).toTracking();
                 final QueryTable resultTable = makeResult(leftTable, rightTable, columnsToAdd, jsm, resultRowSet,
-                        cs -> new CrossJoinRightColumnSource<>(jsm, cs, rightTable.isRefreshing()));
+                        cs -> CrossJoinRightColumnSource.maybeWrap(jsm, cs, rightTable.isRefreshing()));
 
                 jsm.startTrackingPrevValues();
                 final ModifiedColumnSet.Transformer leftTransformer = leftTable.newModifiedColumnSetTransformer(
@@ -290,7 +291,7 @@ public void onUpdate(final TableUpdate upstream) {
             final TrackingWritableRowSet resultRowSet = jsm.build(leftTable, rightTable).toTracking();
 
             final QueryTable resultTable = makeResult(leftTable, rightTable, columnsToAdd, jsm, resultRowSet,
-                    cs -> new CrossJoinRightColumnSource<>(jsm, cs, rightTable.isRefreshing()));
+                    cs -> CrossJoinRightColumnSource.maybeWrap(jsm, cs, rightTable.isRefreshing()));
 
             final ModifiedColumnSet.Transformer rightTransformer =
                     rightTable.newModifiedColumnSetTransformer(resultTable, columnsToAdd);
@@ -1045,7 +1046,7 @@ private static QueryTable zeroKeyColumnsJoin(
         }
 
         final QueryTable result = makeResult(leftTable, rightTable, columnsToAdd, crossJoinState, resultRowSet,
-                cs -> new BitMaskingColumnSource<>(crossJoinState, cs));
+                cs -> BitMaskingColumnSource.maybeWrap(crossJoinState, cs));
 
         if (leftTable.isRefreshing() || rightTable.isRefreshing()) {
             crossJoinState.startTrackingPrevious();
@@ -1409,8 +1410,7 @@ private static <T extends ColumnSource<?>> QueryTable makeResult(
         final Map<String, ColumnSource<?>> columnSourceMap = new LinkedHashMap<>();
 
         for (final Map.Entry<String, ColumnSource<?>> leftColumn : leftTable.getColumnSourceMap().entrySet()) {
-            final BitShiftingColumnSource<?> wrappedSource =
-                    new BitShiftingColumnSource<>(joinState, leftColumn.getValue());
+            final ColumnSource<?> wrappedSource = BitShiftingColumnSource.maybeWrap(joinState, leftColumn.getValue());
             columnSourceMap.put(leftColumn.getKey(), wrappedSource);
         }
 
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/FlattenOperation.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/FlattenOperation.java
index 499cf580af1..ee526102699 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/FlattenOperation.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/FlattenOperation.java
@@ -43,7 +43,7 @@ public Result<QueryTable> initialize(boolean usePrev, long beforeClock) {
         final long size = usePrev ? rowSet.sizePrev() : rowSet.size();
 
         for (Map.Entry<String, ColumnSource<?>> entry : parent.getColumnSourceMap().entrySet()) {
-            resultColumns.put(entry.getKey(), new RedirectedColumnSource<>(rowRedirection, entry.getValue()));
+            resultColumns.put(entry.getKey(), RedirectedColumnSource.maybeRedirect(rowRedirection, entry.getValue()));
         }
 
         resultTable = new QueryTable(RowSetFactory.flat(size).toTracking(), resultColumns);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/NaturalJoinHelper.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/NaturalJoinHelper.java
index c1e5e02d4d0..be3d3cb90e1 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/NaturalJoinHelper.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/NaturalJoinHelper.java
@@ -440,7 +440,8 @@ private static QueryTable makeResult(@NotNull final QueryTable leftTable,
             final boolean rightRefreshingColumns) {
         final Map<String, ColumnSource<?>> columnSourceMap = new LinkedHashMap<>(leftTable.getColumnSourceMap());
         for (MatchPair mp : columnsToAdd) {
-            final RedirectedColumnSource<?> redirectedColumnSource =
+            // note that we must always redirect the right-hand side, because unmatched rows will be redirected to null
+            final ColumnSource<?> redirectedColumnSource =
                     new RedirectedColumnSource<>(rowRedirection, rightTable.getColumnSource(mp.rightColumn()));
             if (rightRefreshingColumns) {
                 redirectedColumnSource.startTrackingPrevValues();
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/QueryTable.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/QueryTable.java
index d6a0d575ec0..da1a97a1391 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/QueryTable.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/QueryTable.java
@@ -2297,7 +2297,7 @@ public Table ungroup(boolean nullFill, Collection<? extends ColumnName> columnsT
                             ungroupedSource.initializeBase(initialBase);
                             result = ungroupedSource;
                         } else {
-                            result = new BitShiftingColumnSource<>(shiftState, column);
+                            result = BitShiftingColumnSource.maybeWrap(shiftState, column);
                         }
                         resultMap.put(name, result);
                     }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/SortOperation.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/SortOperation.java
index 18cc4aba062..ec3db462d80 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/SortOperation.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/SortOperation.java
@@ -116,7 +116,7 @@ private QueryTable historicalSort(SortHelpers.SortMapping sortedKeys) {
         final Map<String, ColumnSource<?>> resultMap = new LinkedHashMap<>();
         for (Map.Entry<String, ColumnSource<?>> stringColumnSourceEntry : parent.getColumnSourceMap().entrySet()) {
             resultMap.put(stringColumnSourceEntry.getKey(),
-                    new RedirectedColumnSource<>(sortMapping, stringColumnSourceEntry.getValue()));
+                    RedirectedColumnSource.maybeRedirect(sortMapping, stringColumnSourceEntry.getValue()));
         }
 
         resultTable = new QueryTable(resultRowSet, resultMap);
@@ -148,7 +148,7 @@ private Result<QueryTable> streamSort(@NotNull final SortHelpers.SortMapping ini
         final Map<String, ColumnSource<?>> resultMap = new LinkedHashMap<>();
         for (Map.Entry<String, ColumnSource<?>> stringColumnSourceEntry : parent.getColumnSourceMap().entrySet()) {
             resultMap.put(stringColumnSourceEntry.getKey(),
-                    new RedirectedColumnSource<>(sortMapping, stringColumnSourceEntry.getValue()));
+                    RedirectedColumnSource.maybeRedirect(sortMapping, stringColumnSourceEntry.getValue()));
         }
 
         resultTable = new QueryTable(resultRowSet, resultMap);
@@ -256,7 +256,7 @@ public Result<QueryTable> initialize(boolean usePrev, long beforeClock) {
 
             for (Map.Entry<String, ColumnSource<?>> stringColumnSourceEntry : parent.getColumnSourceMap().entrySet()) {
                 resultMap.put(stringColumnSourceEntry.getKey(),
-                        new RedirectedColumnSource<>(sortMapping, stringColumnSourceEntry.getValue()));
+                        RedirectedColumnSource.maybeRedirect(sortMapping, stringColumnSourceEntry.getValue()));
             }
 
             // noinspection unchecked
@@ -288,13 +288,20 @@ public Result<QueryTable> initialize(boolean usePrev, long beforeClock) {
         }
     }
 
+    /**
+     * Get the row redirection for a sort result.
+     *
+     * @param sortResult The sort result table; <em>must</em> be the direct result of a sort.
+     * @return The row redirection if at least one column required redirection, otherwise {@code null}
+     */
     public static RowRedirection getRowRedirection(@NotNull final Table sortResult) {
-        final String firstColumnName = sortResult.getDefinition().getColumns().get(0).getName();
-        final ColumnSource<?> firstColumnSource = sortResult.getColumnSource(firstColumnName);
-        if (!(firstColumnSource instanceof RedirectedColumnSource)) {
-            return null;
+        for (final ColumnSource<?> columnSource : sortResult.getColumnSources()) {
+            if (!(columnSource instanceof RedirectedColumnSource)) {
+                continue;
+            }
+            return ((RedirectedColumnSource<?>) columnSource).getRowRedirection();
         }
-        return ((RedirectedColumnSource) firstColumnSource).getRowRedirection();
+        return null;
     }
 
     /**
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/BaseAddOnlyFirstOrLastChunkedOperator.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/BaseAddOnlyFirstOrLastChunkedOperator.java
index b1c88357e58..dd547a33b64 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/BaseAddOnlyFirstOrLastChunkedOperator.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/BaseAddOnlyFirstOrLastChunkedOperator.java
@@ -35,9 +35,8 @@ abstract class BaseAddOnlyFirstOrLastChunkedOperator
 
         this.resultColumns = new LinkedHashMap<>(resultPairs.length);
         for (final MatchPair mp : resultPairs) {
-            // noinspection unchecked
-            resultColumns.put(mp.leftColumn(),
-                    new RedirectedColumnSource(rowRedirection, originalTable.getColumnSource(mp.rightColumn())));
+            resultColumns.put(mp.leftColumn(), RedirectedColumnSource.maybeRedirect(
+                    rowRedirection, originalTable.getColumnSource(mp.rightColumn())));
         }
         if (exposeRedirectionAs != null) {
             resultColumns.put(exposeRedirectionAs, redirections);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/FirstOrLastChunkedOperator.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/FirstOrLastChunkedOperator.java
index 2769566928d..77144d84ea0 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/FirstOrLastChunkedOperator.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/FirstOrLastChunkedOperator.java
@@ -44,9 +44,8 @@ public class FirstOrLastChunkedOperator
 
         this.resultColumns = new LinkedHashMap<>(resultPairs.length);
         for (final MatchPair mp : resultPairs) {
-            // noinspection unchecked
-            resultColumns.put(mp.leftColumn(),
-                    new RedirectedColumnSource(rowRedirection, originalTable.getColumnSource(mp.rightColumn())));
+            resultColumns.put(mp.leftColumn(), RedirectedColumnSource.maybeRedirect(
+                    rowRedirection, originalTable.getColumnSource(mp.rightColumn())));
         }
         exposeRedirections = exposeRedirectionAs != null;
         if (exposeRedirectionAs != null) {
@@ -298,9 +297,8 @@ private class DuplicateOperator implements IterativeChunkedAggregationOperator {
 
         private DuplicateOperator(MatchPair[] resultPairs, Table table, String exposeRedirectionAs) {
             for (final MatchPair mp : resultPairs) {
-                // noinspection unchecked
                 resultColumns.put(mp.leftColumn(),
-                        new RedirectedColumnSource(rowRedirection, table.getColumnSource(mp.rightColumn())));
+                        RedirectedColumnSource.maybeRedirect(rowRedirection, table.getColumnSource(mp.rightColumn())));
             }
             if (exposeRedirectionAs != null) {
                 resultColumns.put(exposeRedirectionAs, redirections);
@@ -466,9 +464,8 @@ private ComplementaryOperator(boolean isFirst, MatchPair[] resultPairs, Table ta
 
             this.resultColumns = new LinkedHashMap<>(resultPairs.length);
             for (final MatchPair mp : resultPairs) {
-                // noinspection unchecked
                 resultColumns.put(mp.leftColumn(),
-                        new RedirectedColumnSource(rowRedirection, table.getColumnSource(mp.rightColumn())));
+                        RedirectedColumnSource.maybeRedirect(rowRedirection, table.getColumnSource(mp.rightColumn())));
             }
             exposeRedirections = exposeRedirectionAs != null;
             if (exposeRedirections) {
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/IncrementalChunkedOperatorAggregationStateManagerOpenAddressedBase.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/IncrementalChunkedOperatorAggregationStateManagerOpenAddressedBase.java
index 8c6f5fd55a2..90b672d798b 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/IncrementalChunkedOperatorAggregationStateManagerOpenAddressedBase.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/IncrementalChunkedOperatorAggregationStateManagerOpenAddressedBase.java
@@ -140,7 +140,8 @@ public ColumnSource[] getKeyHashTableSources() {
                         alternateKeySources[kci], mainKeySources[kci]);
             }
             // noinspection unchecked
-            keyHashTableSources[kci] = new RedirectedColumnSource(resultIndexToHashSlot, alternatingColumnSources[kci]);
+            keyHashTableSources[kci] =
+                    RedirectedColumnSource.maybeRedirect(resultIndexToHashSlot, alternatingColumnSources[kci]);
         }
 
         return keyHashTableSources;
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/IncrementalChunkedOperatorAggregationStateManagerTypedBase.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/IncrementalChunkedOperatorAggregationStateManagerTypedBase.java
index f6ce5d37929..c0fc54b923d 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/IncrementalChunkedOperatorAggregationStateManagerTypedBase.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/IncrementalChunkedOperatorAggregationStateManagerTypedBase.java
@@ -76,7 +76,7 @@ public ColumnSource[] getKeyHashTableSources() {
         final ColumnSource[] keyHashTableSources = new ColumnSource[mainKeySources.length];
         for (int kci = 0; kci < mainKeySources.length; ++kci) {
             // noinspection unchecked
-            keyHashTableSources[kci] = new RedirectedColumnSource(resultRowKeyToHashSlot,
+            keyHashTableSources[kci] = RedirectedColumnSource.maybeRedirect(resultRowKeyToHashSlot,
                     new HashTableColumnSource(mainKeySources[kci], overflowKeySources[kci]));
         }
         return keyHashTableSources;
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/SortedFirstOrLastChunkedOperator.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/SortedFirstOrLastChunkedOperator.java
index daa30528726..c86df1511ca 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/SortedFirstOrLastChunkedOperator.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/SortedFirstOrLastChunkedOperator.java
@@ -49,9 +49,8 @@ public class SortedFirstOrLastChunkedOperator
 
         this.resultColumns = new LinkedHashMap<>();
         for (final MatchPair mp : resultNames) {
-            // noinspection unchecked,rawtypes
-            resultColumns.put(mp.leftColumn(),
-                    new RedirectedColumnSource(rowRedirection, originalTable.getColumnSource(mp.rightColumn())));
+            resultColumns.put(mp.leftColumn(), RedirectedColumnSource.maybeRedirect(
+                    rowRedirection, originalTable.getColumnSource(mp.rightColumn())));
         }
     }
 
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/StaticChunkedOperatorAggregationStateManagerOpenAddressedBase.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/StaticChunkedOperatorAggregationStateManagerOpenAddressedBase.java
index 13a9134b9c7..7e5e2f86fa6 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/StaticChunkedOperatorAggregationStateManagerOpenAddressedBase.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/StaticChunkedOperatorAggregationStateManagerOpenAddressedBase.java
@@ -69,7 +69,7 @@ public ColumnSource[] getKeyHashTableSources() {
         final ColumnSource[] keyHashTableSources = new ColumnSource[mainKeySources.length];
         for (int kci = 0; kci < mainKeySources.length; ++kci) {
             // noinspection unchecked
-            keyHashTableSources[kci] = new RedirectedColumnSource(resultIndexToHashSlot, mainKeySources[kci]);
+            keyHashTableSources[kci] = RedirectedColumnSource.maybeRedirect(resultIndexToHashSlot, mainKeySources[kci]);
         }
         return keyHashTableSources;
     }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/StaticChunkedOperatorAggregationStateManagerTypedBase.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/StaticChunkedOperatorAggregationStateManagerTypedBase.java
index 32a3a555d74..26d492d449f 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/StaticChunkedOperatorAggregationStateManagerTypedBase.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/StaticChunkedOperatorAggregationStateManagerTypedBase.java
@@ -123,7 +123,7 @@ public ColumnSource[] getKeyHashTableSources() {
         final ColumnSource[] keyHashTableSources = new ColumnSource[mainKeySources.length];
         for (int kci = 0; kci < mainKeySources.length; ++kci) {
             // noinspection unchecked
-            keyHashTableSources[kci] = new RedirectedColumnSource(resultIndexToHashSlot,
+            keyHashTableSources[kci] = RedirectedColumnSource.maybeRedirect(resultIndexToHashSlot,
                     new HashTableColumnSource(mainKeySources[kci], overflowKeySources[kci]));
         }
         return keyHashTableSources;
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/remote/InitialSnapshotTable.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/remote/InitialSnapshotTable.java
index e15c2567830..0cf2b3bfab0 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/remote/InitialSnapshotTable.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/remote/InitialSnapshotTable.java
@@ -195,7 +195,7 @@ public static InitialSnapshotTable setupInitialSnapshotTable(
             writableSources[ci] = ArrayBackedColumnSource.getMemoryColumnSource(
                     0, column.getDataType(), column.getComponentType());
             finalColumns.put(column.getName(),
-                    new WritableRedirectedColumnSource<>(rowRedirection, writableSources[ci], 0));
+                    WritableRedirectedColumnSource.maybeRedirect(rowRedirection, writableSources[ci], 0));
         }
         // This table does not run, so we don't need to tell our row redirection or column source to start
         // tracking
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/replay/QueryReplayGroupedTable.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/replay/QueryReplayGroupedTable.java
index f03d51ac41a..7b4ee746350 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/replay/QueryReplayGroupedTable.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/replay/QueryReplayGroupedTable.java
@@ -34,7 +34,7 @@ private static Map<String, ColumnSource<?>> getResultSources(Map<String, ? exten
         Map<String, ColumnSource<?>> result = new LinkedHashMap<>();
         for (Map.Entry<String, ? extends ColumnSource<?>> stringEntry : input.entrySet()) {
             ColumnSource<?> value = stringEntry.getValue();
-            result.put(stringEntry.getKey(), new RedirectedColumnSource<>(rowRedirection, value));
+            result.put(stringEntry.getKey(), RedirectedColumnSource.maybeRedirect(rowRedirection, value));
         }
         return result;
     }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/analyzers/SelectAndViewAnalyzer.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/analyzers/SelectAndViewAnalyzer.java
index 194c34bbc69..4e6ea764e14 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/analyzers/SelectAndViewAnalyzer.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/analyzers/SelectAndViewAnalyzer.java
@@ -167,7 +167,7 @@ public static SelectAndViewAnalyzer create(final Mode mode, final Map<String, Co
                 }
                 case SELECT_REDIRECTED_STATIC: {
                     final WritableColumnSource<?> underlyingSource = sc.newDestInstance(rowSet.size());
-                    final WritableColumnSource<?> scs = new WritableRedirectedColumnSource<>(
+                    final WritableColumnSource<?> scs = WritableRedirectedColumnSource.maybeRedirect(
                             rowRedirection, underlyingSource, rowSet.size());
                     analyzer =
                             analyzer.createLayerForSelect(rowSet, sc.getName(), sc, scs, underlyingSource, distinctDeps,
@@ -183,7 +183,8 @@ public static SelectAndViewAnalyzer create(final Mode mode, final Map<String, Co
                     WritableColumnSource<?> underlyingSource = null;
                     if (rowRedirection != null) {
                         underlyingSource = scs;
-                        scs = new WritableRedirectedColumnSource<>(rowRedirection, underlyingSource, rowSet.intSize());
+                        scs = WritableRedirectedColumnSource.maybeRedirect(
+                                rowRedirection, underlyingSource, rowSet.intSize());
                     }
                     analyzer =
                             analyzer.createLayerForSelect(rowSet, sc.getName(), sc, scs, underlyingSource, distinctDeps,
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/analyzers/StaticFlattenLayer.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/analyzers/StaticFlattenLayer.java
index e9e2f95294b..ba4d224f4c9 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/analyzers/StaticFlattenLayer.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/analyzers/StaticFlattenLayer.java
@@ -48,7 +48,7 @@ final public class StaticFlattenLayer extends SelectAndViewAnalyzer {
                 return;
             }
 
-            overriddenColumns.put(name, new RedirectedColumnSource<>(rowRedirection, cs));
+            overriddenColumns.put(name, RedirectedColumnSource.maybeRedirect(rowRedirection, cs));
         });
     }
 
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitMaskingColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitMaskingColumnSource.java
index 29e6b385f55..95643096d0e 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitMaskingColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitMaskingColumnSource.java
@@ -21,10 +21,25 @@
 
 public class BitMaskingColumnSource<T> extends AbstractColumnSource<T> implements UngroupableColumnSource {
 
+    /**
+     * Wrap the innerSource if it is not agnostic to redirection. Otherwise, return the innerSource.
+     *
+     * @param shiftState The cross join shift state to use
+     * @param innerSource The column source to redirect
+     */
+    public static <T> ColumnSource<T> maybeWrap(
+            final ZeroKeyCrossJoinShiftState shiftState,
+            @NotNull final ColumnSource<T> innerSource) {
+        if (innerSource instanceof RowKeyAgnosticColumnSource) {
+            return innerSource;
+        }
+        return new BitMaskingColumnSource<>(shiftState, innerSource);
+    }
+
     private final ZeroKeyCrossJoinShiftState shiftState;
     private final ColumnSource<T> innerSource;
 
-    public BitMaskingColumnSource(
+    protected BitMaskingColumnSource(
             final ZeroKeyCrossJoinShiftState shiftState,
             @NotNull final ColumnSource<T> innerSource) {
         super(innerSource.getType());
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitShiftingColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitShiftingColumnSource.java
index 5fec3f136c7..e461dd19361 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitShiftingColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitShiftingColumnSource.java
@@ -24,10 +24,27 @@
 
 public class BitShiftingColumnSource<T> extends AbstractColumnSource<T> implements UngroupableColumnSource {
 
+    /**
+     * Wrap the innerSource if it is not agnostic to redirection. Otherwise, return the innerSource.
+     *
+     * @param shiftState The cross join shift state to use
+     * @param innerSource The column source to redirect
+     */
+    public static <T> ColumnSource<T> maybeWrap(
+            @NotNull final CrossJoinShiftState shiftState,
+            @NotNull final ColumnSource<T> innerSource) {
+        if (innerSource instanceof RowKeyAgnosticColumnSource) {
+            return innerSource;
+        }
+        return new BitShiftingColumnSource<>(shiftState, innerSource);
+    }
+
     private final CrossJoinShiftState shiftState;
     private final ColumnSource<T> innerSource;
 
-    public BitShiftingColumnSource(final CrossJoinShiftState shiftState, @NotNull final ColumnSource<T> innerSource) {
+    protected BitShiftingColumnSource(
+            @NotNull final CrossJoinShiftState shiftState,
+            @NotNull final ColumnSource<T> innerSource) {
         super(innerSource.getType());
         this.shiftState = shiftState;
         this.innerSource = innerSource;
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BooleanSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BooleanSingleValueSource.java
new file mode 100644
index 00000000000..f39f1ee3925
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BooleanSingleValueSource.java
@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.engine.table.impl.sources;
+
+import io.deephaven.chunk.Chunk;
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.ObjectChunk;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.chunk.WritableObjectChunk;
+import io.deephaven.chunk.attributes.Values;
+import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.RowKeys;
+import io.deephaven.engine.table.impl.MutableColumnSourceGetDefaults;
+import io.deephaven.engine.updategraph.LogicalClock;
+import io.deephaven.util.QueryConstants;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Single value source for Boolean.
+ */
+public class BooleanSingleValueSource extends SingleValueColumnSource<Boolean> implements MutableColumnSourceGetDefaults.ForBoolean {
+    private Boolean current;
+    private transient Boolean prev;
+
+    BooleanSingleValueSource() {
+        super(Boolean.class);
+        current = QueryConstants.NULL_BOOLEAN;
+        prev = QueryConstants.NULL_BOOLEAN;
+    }
+
+    @Nullable
+    @Override
+    public Boolean get(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return QueryConstants.NULL_BOOLEAN;
+        }
+        return current;
+    }
+
+    @Nullable
+    @Override
+    public Boolean getPrev(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return QueryConstants.NULL_BOOLEAN;
+        }
+        if (!isTrackingPrevValues || changeTime < LogicalClock.DEFAULT.currentStep()) {
+            return current;
+        }
+        return prev;
+    }
+
+    @Override
+    public final void set(Boolean value) {
+        if (isTrackingPrevValues) {
+            final long currentStep = LogicalClock.DEFAULT.currentStep();
+            if (changeTime < currentStep) {
+                prev = current;
+                changeTime = currentStep;
+            }
+        }
+        current = value;
+    }
+
+    @Override
+    public final void set(long key, Boolean value) {
+        set(value);
+    }
+
+    @Override
+    public void setNull(long key) {
+        // region null set
+        set(QueryConstants.NULL_BOOLEAN);
+        // endregion null set
+    }
+
+    @Override
+    public final void fillFromChunk(
+            @NotNull FillFromContext context,
+            @NotNull Chunk<? extends Values> src,
+            @NotNull RowSequence orderedKeys) {
+        if (orderedKeys.isEmpty()) {
+            return;
+        }
+
+        // We can only hold one value anyway, so arbitrarily take the first value in the chunk and ignore the rest.
+        final ObjectChunk<Boolean, ? extends Values> chunk = src.asObjectChunk();
+        set(chunk.get(0));
+    }
+
+    @Override
+    public void fillFromChunkUnordered(
+            @NotNull FillFromContext context,
+            @NotNull Chunk<? extends Values> src,
+            @NotNull LongChunk<RowKeys> keys) {
+        if (keys.size() == 0) {
+            return;
+        }
+        // We can only hold one value anyway, so arbitrarily take the first value in the chunk and ignore the rest.
+        final ObjectChunk<Boolean, ? extends Values> chunk = src.asObjectChunk();
+        set(chunk.get(0));
+    }
+
+    @Override
+    public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
+            @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), get(0));
+    }
+
+    @Override
+    public void fillPrevChunk(@NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), get(0));
+    }
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        Boolean value = get(0);
+        final WritableObjectChunk<Boolean, ? super Values> destChunk = dest.asWritableObjectChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? null : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        Boolean value = getPrev(0);
+        final WritableObjectChunk<Boolean, ? super Values> destChunk = dest.asWritableObjectChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? null : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ByteSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ByteSingleValueSource.java
index 7ac7717420a..c08f5404b2a 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ByteSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ByteSingleValueSource.java
@@ -8,12 +8,12 @@
  */
 package io.deephaven.engine.table.impl.sources;
 
+import io.deephaven.chunk.WritableByteChunk;
+import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
-import io.deephaven.engine.table.ColumnSource;
 import io.deephaven.engine.table.impl.MutableColumnSourceGetDefaults;
 import io.deephaven.engine.updategraph.LogicalClock;
 import io.deephaven.engine.rowset.chunkattributes.RowKeys;
-import io.deephaven.util.QueryConstants;
 import io.deephaven.chunk.ByteChunk;
 import io.deephaven.chunk.Chunk;
 import io.deephaven.chunk.LongChunk;
@@ -89,11 +89,17 @@ public final void setNull(long key) {
 
     @Override
     public final byte getByte(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_BYTE;
+        }
         return current;
     }
 
     @Override
     public final byte getPrevByte(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_BYTE;
+        }
         if (!isTrackingPrevValues || changeTime < LogicalClock.DEFAULT.currentStep()) {
             return current;
         }
@@ -119,4 +125,49 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
         final ByteChunk<? extends Values> chunk = src.asByteChunk();
         set(chunk.get(0));
     }
+
+    @Override
+    public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
+            @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableByteChunk().fillWithValue(0, rowSequence.intSize(), getByte(0));
+    }
+
+    @Override
+    public void fillPrevChunk(@NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableByteChunk().fillWithValue(0, rowSequence.intSize(), getPrevByte(0));
+    }
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        byte value = getByte(0);
+        final WritableByteChunk<? super Values> destChunk = dest.asWritableByteChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_BYTE : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        byte value = getPrevByte(0);
+        final WritableByteChunk<? super Values> destChunk = dest.asWritableByteChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_BYTE : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CharacterSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CharacterSingleValueSource.java
index 330ed39e385..0cde437c6b9 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CharacterSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CharacterSingleValueSource.java
@@ -3,12 +3,12 @@
  */
 package io.deephaven.engine.table.impl.sources;
 
+import io.deephaven.chunk.WritableCharChunk;
+import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
-import io.deephaven.engine.table.ColumnSource;
 import io.deephaven.engine.table.impl.MutableColumnSourceGetDefaults;
 import io.deephaven.engine.updategraph.LogicalClock;
 import io.deephaven.engine.rowset.chunkattributes.RowKeys;
-import io.deephaven.util.QueryConstants;
 import io.deephaven.chunk.CharChunk;
 import io.deephaven.chunk.Chunk;
 import io.deephaven.chunk.LongChunk;
@@ -84,11 +84,17 @@ public final void setNull(long key) {
 
     @Override
     public final char getChar(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_CHAR;
+        }
         return current;
     }
 
     @Override
     public final char getPrevChar(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_CHAR;
+        }
         if (!isTrackingPrevValues || changeTime < LogicalClock.DEFAULT.currentStep()) {
             return current;
         }
@@ -114,4 +120,49 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
         final CharChunk<? extends Values> chunk = src.asCharChunk();
         set(chunk.get(0));
     }
+
+    @Override
+    public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
+            @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableCharChunk().fillWithValue(0, rowSequence.intSize(), getChar(0));
+    }
+
+    @Override
+    public void fillPrevChunk(@NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableCharChunk().fillWithValue(0, rowSequence.intSize(), getPrevChar(0));
+    }
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        char value = getChar(0);
+        final WritableCharChunk<? super Values> destChunk = dest.asWritableCharChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_CHAR : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        char value = getPrevChar(0);
+        final WritableCharChunk<? super Values> destChunk = dest.asWritableCharChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_CHAR : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CrossJoinRightColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CrossJoinRightColumnSource.java
index d732c74ed04..a6b437a0b90 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CrossJoinRightColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CrossJoinRightColumnSource.java
@@ -29,6 +29,7 @@
 import io.deephaven.engine.rowset.RowSet;
 import io.deephaven.engine.rowset.TrackingRowSet;
 import io.deephaven.engine.table.impl.util.ChunkUtils;
+import io.deephaven.proto.backplane.grpc.NullValue;
 import org.apache.commons.lang3.mutable.MutableInt;
 import org.apache.commons.lang3.mutable.MutableLong;
 import org.jetbrains.annotations.NotNull;
@@ -36,13 +37,34 @@
 import static io.deephaven.util.QueryConstants.*;
 
 public class CrossJoinRightColumnSource<T> extends AbstractColumnSource<T> implements UngroupableColumnSource {
+
+    /**
+     * Wrap the innerSource if it is not agnostic to redirection. Otherwise, return the innerSource.
+     *
+     * @param crossJoinManager The cross join manager to use
+     * @param innerSource The column source to redirect
+     * @param rightIsLive Whether the right side is live
+     */
+    public static <T> ColumnSource<T> maybeWrap(
+            @NotNull final CrossJoinStateManager crossJoinManager,
+            @NotNull final ColumnSource<T> innerSource,
+            boolean rightIsLive) {
+        // Force wrapping if this is a leftOuterJoin or else we will not see the nulls; unless every row is null.
+        if ((!crossJoinManager.leftOuterJoin() && innerSource instanceof RowKeyAgnosticColumnSource)
+                || innerSource instanceof NullValueColumnSource) {
+            return innerSource;
+        }
+        return new CrossJoinRightColumnSource<>(crossJoinManager, innerSource, rightIsLive);
+    }
+
     private final boolean rightIsLive;
     private final CrossJoinStateManager crossJoinManager;
     protected final ColumnSource<T> innerSource;
 
-
-    public CrossJoinRightColumnSource(@NotNull final CrossJoinStateManager crossJoinManager,
-            @NotNull final ColumnSource<T> innerSource, boolean rightIsLive) {
+    protected CrossJoinRightColumnSource(
+            @NotNull final CrossJoinStateManager crossJoinManager,
+            @NotNull final ColumnSource<T> innerSource,
+            boolean rightIsLive) {
         super(innerSource.getType());
         this.rightIsLive = rightIsLive;
         this.crossJoinManager = crossJoinManager;
@@ -397,7 +419,6 @@ private static class FillContext implements ColumnSource.FillContext {
         private final ResettableWritableChunk<Values> innerOrderedValuesSlice;
         private final DupExpandKernel dupExpandKernel;
         private final PermuteKernel permuteKernel;
-        private final boolean allowRightSideNulls;
 
         FillContext(final CrossJoinRightColumnSource<?> cs, final int chunkCapacity,
                 final SharedContext sharedContext) {
@@ -420,7 +441,6 @@ private static class FillContext implements ColumnSource.FillContext {
                 dupExpandKernel = DupExpandKernel.makeDupExpand(cs.getChunkType());
                 permuteKernel = PermuteKernel.makePermuteKernel(cs.getChunkType());
             }
-            allowRightSideNulls = cs.crossJoinManager.leftOuterJoin();
         }
 
         @Override
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/DoubleSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/DoubleSingleValueSource.java
index b6e28cd2c91..bff59af4298 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/DoubleSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/DoubleSingleValueSource.java
@@ -8,12 +8,12 @@
  */
 package io.deephaven.engine.table.impl.sources;
 
+import io.deephaven.chunk.WritableDoubleChunk;
+import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
-import io.deephaven.engine.table.ColumnSource;
 import io.deephaven.engine.table.impl.MutableColumnSourceGetDefaults;
 import io.deephaven.engine.updategraph.LogicalClock;
 import io.deephaven.engine.rowset.chunkattributes.RowKeys;
-import io.deephaven.util.QueryConstants;
 import io.deephaven.chunk.DoubleChunk;
 import io.deephaven.chunk.Chunk;
 import io.deephaven.chunk.LongChunk;
@@ -89,11 +89,17 @@ public final void setNull(long key) {
 
     @Override
     public final double getDouble(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_DOUBLE;
+        }
         return current;
     }
 
     @Override
     public final double getPrevDouble(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_DOUBLE;
+        }
         if (!isTrackingPrevValues || changeTime < LogicalClock.DEFAULT.currentStep()) {
             return current;
         }
@@ -119,4 +125,49 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
         final DoubleChunk<? extends Values> chunk = src.asDoubleChunk();
         set(chunk.get(0));
     }
+
+    @Override
+    public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
+            @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableDoubleChunk().fillWithValue(0, rowSequence.intSize(), getDouble(0));
+    }
+
+    @Override
+    public void fillPrevChunk(@NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableDoubleChunk().fillWithValue(0, rowSequence.intSize(), getPrevDouble(0));
+    }
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        double value = getDouble(0);
+        final WritableDoubleChunk<? super Values> destChunk = dest.asWritableDoubleChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_DOUBLE : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        double value = getPrevDouble(0);
+        final WritableDoubleChunk<? super Values> destChunk = dest.asWritableDoubleChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_DOUBLE : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/FloatSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/FloatSingleValueSource.java
index 7c9aac9336a..9d10fb61b1e 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/FloatSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/FloatSingleValueSource.java
@@ -8,12 +8,12 @@
  */
 package io.deephaven.engine.table.impl.sources;
 
+import io.deephaven.chunk.WritableFloatChunk;
+import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
-import io.deephaven.engine.table.ColumnSource;
 import io.deephaven.engine.table.impl.MutableColumnSourceGetDefaults;
 import io.deephaven.engine.updategraph.LogicalClock;
 import io.deephaven.engine.rowset.chunkattributes.RowKeys;
-import io.deephaven.util.QueryConstants;
 import io.deephaven.chunk.FloatChunk;
 import io.deephaven.chunk.Chunk;
 import io.deephaven.chunk.LongChunk;
@@ -89,11 +89,17 @@ public final void setNull(long key) {
 
     @Override
     public final float getFloat(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_FLOAT;
+        }
         return current;
     }
 
     @Override
     public final float getPrevFloat(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_FLOAT;
+        }
         if (!isTrackingPrevValues || changeTime < LogicalClock.DEFAULT.currentStep()) {
             return current;
         }
@@ -119,4 +125,49 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
         final FloatChunk<? extends Values> chunk = src.asFloatChunk();
         set(chunk.get(0));
     }
+
+    @Override
+    public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
+            @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableFloatChunk().fillWithValue(0, rowSequence.intSize(), getFloat(0));
+    }
+
+    @Override
+    public void fillPrevChunk(@NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableFloatChunk().fillWithValue(0, rowSequence.intSize(), getPrevFloat(0));
+    }
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        float value = getFloat(0);
+        final WritableFloatChunk<? super Values> destChunk = dest.asWritableFloatChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_FLOAT : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        float value = getPrevFloat(0);
+        final WritableFloatChunk<? super Values> destChunk = dest.asWritableFloatChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_FLOAT : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/IntegerSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/IntegerSingleValueSource.java
index 6a1b5cbe69b..21ded020289 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/IntegerSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/IntegerSingleValueSource.java
@@ -8,12 +8,12 @@
  */
 package io.deephaven.engine.table.impl.sources;
 
+import io.deephaven.chunk.WritableIntChunk;
+import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
-import io.deephaven.engine.table.ColumnSource;
 import io.deephaven.engine.table.impl.MutableColumnSourceGetDefaults;
 import io.deephaven.engine.updategraph.LogicalClock;
 import io.deephaven.engine.rowset.chunkattributes.RowKeys;
-import io.deephaven.util.QueryConstants;
 import io.deephaven.chunk.IntChunk;
 import io.deephaven.chunk.Chunk;
 import io.deephaven.chunk.LongChunk;
@@ -89,11 +89,17 @@ public final void setNull(long key) {
 
     @Override
     public final int getInt(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_INT;
+        }
         return current;
     }
 
     @Override
     public final int getPrevInt(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_INT;
+        }
         if (!isTrackingPrevValues || changeTime < LogicalClock.DEFAULT.currentStep()) {
             return current;
         }
@@ -119,4 +125,49 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
         final IntChunk<? extends Values> chunk = src.asIntChunk();
         set(chunk.get(0));
     }
+
+    @Override
+    public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
+            @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableIntChunk().fillWithValue(0, rowSequence.intSize(), getInt(0));
+    }
+
+    @Override
+    public void fillPrevChunk(@NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableIntChunk().fillWithValue(0, rowSequence.intSize(), getPrevInt(0));
+    }
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        int value = getInt(0);
+        final WritableIntChunk<? super Values> destChunk = dest.asWritableIntChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_INT : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        int value = getPrevInt(0);
+        final WritableIntChunk<? super Values> destChunk = dest.asWritableIntChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_INT : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/LongSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/LongSingleValueSource.java
index 7891841cda1..ab7df16511e 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/LongSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/LongSingleValueSource.java
@@ -8,12 +8,12 @@
  */
 package io.deephaven.engine.table.impl.sources;
 
+import io.deephaven.chunk.WritableLongChunk;
+import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
-import io.deephaven.engine.table.ColumnSource;
 import io.deephaven.engine.table.impl.MutableColumnSourceGetDefaults;
 import io.deephaven.engine.updategraph.LogicalClock;
 import io.deephaven.engine.rowset.chunkattributes.RowKeys;
-import io.deephaven.util.QueryConstants;
 import io.deephaven.chunk.LongChunk;
 import io.deephaven.chunk.Chunk;
 import io.deephaven.chunk.LongChunk;
@@ -89,11 +89,17 @@ public final void setNull(long key) {
 
     @Override
     public final long getLong(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_LONG;
+        }
         return current;
     }
 
     @Override
     public final long getPrevLong(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_LONG;
+        }
         if (!isTrackingPrevValues || changeTime < LogicalClock.DEFAULT.currentStep()) {
             return current;
         }
@@ -119,4 +125,49 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
         final LongChunk<? extends Values> chunk = src.asLongChunk();
         set(chunk.get(0));
     }
+
+    @Override
+    public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
+            @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableLongChunk().fillWithValue(0, rowSequence.intSize(), getLong(0));
+    }
+
+    @Override
+    public void fillPrevChunk(@NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableLongChunk().fillWithValue(0, rowSequence.intSize(), getPrevLong(0));
+    }
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        long value = getLong(0);
+        final WritableLongChunk<? super Values> destChunk = dest.asWritableLongChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_LONG : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        long value = getPrevLong(0);
+        final WritableLongChunk<? super Values> destChunk = dest.asWritableLongChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_LONG : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/NullValueColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/NullValueColumnSource.java
index e69eb0bb64d..003042f2779 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/NullValueColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/NullValueColumnSource.java
@@ -5,7 +5,9 @@
 
 import io.deephaven.base.Pair;
 import io.deephaven.base.verify.Assert;
+import io.deephaven.chunk.LongChunk;
 import io.deephaven.chunk.attributes.Values;
+import io.deephaven.engine.rowset.chunkattributes.RowKeys;
 import io.deephaven.engine.table.ColumnSource;
 import io.deephaven.engine.table.ColumnDefinition;
 import io.deephaven.engine.table.TableDefinition;
@@ -26,7 +28,7 @@
  * A column source that returns null for all keys.
  */
 public class NullValueColumnSource<T> extends AbstractColumnSource<T>
-        implements ShiftData.ShiftCallback, InMemoryColumnSource {
+        implements ShiftData.ShiftCallback, RowKeyAgnosticColumnSource<Values> {
     private static final KeyedObjectKey.Basic<Pair<Class<?>, Class<?>>, NullValueColumnSource<?>> KEY_TYPE =
             new KeyedObjectKey.Basic<>() {
                 @Override
@@ -192,4 +194,23 @@ public void fillPrevChunk(@NotNull FillContext context,
             @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
         fillChunk(context, destination, rowSequence);
     }
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // note that we do not need to look for RowSequence.NULL_ROW_KEY; all values are null
+        destination.setSize(keys.size());
+        destination.fillWithNullValue(0, keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        fillChunkUnordered(context, destination, keys);
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ObjectSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ObjectSingleValueSource.java
index e06b0f04b5a..01e0e1e7bec 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ObjectSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ObjectSingleValueSource.java
@@ -8,12 +8,12 @@
  */
 package io.deephaven.engine.table.impl.sources;
 
+import io.deephaven.chunk.WritableObjectChunk;
+import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
-import io.deephaven.engine.table.ColumnSource;
 import io.deephaven.engine.table.impl.MutableColumnSourceGetDefaults;
 import io.deephaven.engine.updategraph.LogicalClock;
 import io.deephaven.engine.rowset.chunkattributes.RowKeys;
-import io.deephaven.util.QueryConstants;
 import io.deephaven.chunk.ObjectChunk;
 import io.deephaven.chunk.Chunk;
 import io.deephaven.chunk.LongChunk;
@@ -77,11 +77,17 @@ public final void setNull(long key) {
 
     @Override
     public final T get(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return null;
+        }
         return current;
     }
 
     @Override
     public final T getPrev(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return null;
+        }
         if (!isTrackingPrevValues || changeTime < LogicalClock.DEFAULT.currentStep()) {
             return current;
         }
@@ -107,4 +113,49 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
         final ObjectChunk<T, ? extends Values> chunk = src.asObjectChunk();
         set(chunk.get(0));
     }
+
+    @Override
+    public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
+            @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), get(0));
+    }
+
+    @Override
+    public void fillPrevChunk(@NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), getPrev(0));
+    }
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        T value = get(0);
+        final WritableObjectChunk<T, ? super Values> destChunk = dest.asWritableObjectChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? null : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        T value = getPrev(0);
+        final WritableObjectChunk<T, ? super Values> destChunk = dest.asWritableObjectChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? null : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RedirectedColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RedirectedColumnSource.java
index e10b4b7c1a8..da3a359c279 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RedirectedColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RedirectedColumnSource.java
@@ -8,6 +8,7 @@
 import io.deephaven.engine.table.SharedContext;
 import io.deephaven.engine.table.ColumnSource;
 import io.deephaven.engine.table.Table;
+import io.deephaven.engine.table.WritableColumnSource;
 import io.deephaven.engine.table.impl.util.RowRedirection;
 import io.deephaven.util.BooleanUtils;
 import io.deephaven.engine.table.impl.join.dupexpand.DupExpandKernel;
@@ -35,11 +36,27 @@
  */
 public class RedirectedColumnSource<T> extends AbstractDeferredGroupingColumnSource<T>
         implements UngroupableColumnSource {
+    /**
+     * Redirect the innerSource if it is not agnostic to redirection. Otherwise, return the innerSource.
+     *
+     * @param rowRedirection The row redirection to use
+     * @param innerSource The column source to redirect
+     */
+    public static <T> ColumnSource<T> maybeRedirect(
+            @NotNull final RowRedirection rowRedirection,
+            @NotNull final ColumnSource<T> innerSource) {
+        if (innerSource instanceof RowKeyAgnosticColumnSource) {
+            return innerSource;
+        }
+        return new RedirectedColumnSource<>(rowRedirection, innerSource);
+    }
+
     protected final RowRedirection rowRedirection;
     protected final ColumnSource<T> innerSource;
     private final boolean ascendingMapping;
 
-    public RedirectedColumnSource(@NotNull final RowRedirection rowRedirection,
+    public RedirectedColumnSource(
+            @NotNull final RowRedirection rowRedirection,
             @NotNull final ColumnSource<T> innerSource) {
         super(innerSource.getType());
         this.rowRedirection = rowRedirection;
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RowKeyAgnosticColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RowKeyAgnosticColumnSource.java
new file mode 100644
index 00000000000..726872f4dbf
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RowKeyAgnosticColumnSource.java
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.engine.table.impl.sources;
+
+import io.deephaven.chunk.attributes.Any;
+
+/**
+ * This is a marker interface for column sources that are agnostic when fulfilling requested row keys.
+ *
+ * The marker extends from {@link InMemoryColumnSource} whether the column source is actually in memory or not; it would
+ * be a waste to materialize the same value for all rows via select.
+ */
+public interface RowKeyAgnosticColumnSource<ATTR extends Any> extends FillUnordered<ATTR>, InMemoryColumnSource {
+
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ShortSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ShortSingleValueSource.java
index ae99200ca48..8a54ec8aa43 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ShortSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ShortSingleValueSource.java
@@ -8,12 +8,12 @@
  */
 package io.deephaven.engine.table.impl.sources;
 
+import io.deephaven.chunk.WritableShortChunk;
+import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
-import io.deephaven.engine.table.ColumnSource;
 import io.deephaven.engine.table.impl.MutableColumnSourceGetDefaults;
 import io.deephaven.engine.updategraph.LogicalClock;
 import io.deephaven.engine.rowset.chunkattributes.RowKeys;
-import io.deephaven.util.QueryConstants;
 import io.deephaven.chunk.ShortChunk;
 import io.deephaven.chunk.Chunk;
 import io.deephaven.chunk.LongChunk;
@@ -89,11 +89,17 @@ public final void setNull(long key) {
 
     @Override
     public final short getShort(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_SHORT;
+        }
         return current;
     }
 
     @Override
     public final short getPrevShort(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_SHORT;
+        }
         if (!isTrackingPrevValues || changeTime < LogicalClock.DEFAULT.currentStep()) {
             return current;
         }
@@ -119,4 +125,49 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
         final ShortChunk<? extends Values> chunk = src.asShortChunk();
         set(chunk.get(0));
     }
+
+    @Override
+    public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
+            @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableShortChunk().fillWithValue(0, rowSequence.intSize(), getShort(0));
+    }
+
+    @Override
+    public void fillPrevChunk(@NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        destination.setSize(rowSequence.intSize());
+        destination.asWritableShortChunk().fillWithValue(0, rowSequence.intSize(), getPrevShort(0));
+    }
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        short value = getShort(0);
+        final WritableShortChunk<? super Values> destChunk = dest.asWritableShortChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_SHORT : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        short value = getPrevShort(0);
+        final WritableShortChunk<? super Values> destChunk = dest.asWritableShortChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_SHORT : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/SingleValueColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/SingleValueColumnSource.java
index 8be483202ce..bf1a924604d 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/SingleValueColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/SingleValueColumnSource.java
@@ -4,13 +4,15 @@
 package io.deephaven.engine.table.impl.sources;
 
 import io.deephaven.chunk.attributes.Values;
+import io.deephaven.engine.rowset.RowSequence;
 import io.deephaven.engine.table.ChunkSink;
 import io.deephaven.engine.table.WritableColumnSource;
 import io.deephaven.engine.table.impl.AbstractColumnSource;
 import io.deephaven.engine.table.impl.util.ShiftData;
 
 public abstract class SingleValueColumnSource<T> extends AbstractColumnSource<T>
-        implements WritableColumnSource<T>, ChunkSink<Values>, ShiftData.ShiftCallback, InMemoryColumnSource {
+        implements WritableColumnSource<T>, ChunkSink<Values>, ShiftData.ShiftCallback,
+        RowKeyAgnosticColumnSource<Values> {
 
     protected transient long changeTime;
     protected boolean isTrackingPrevValues;
@@ -43,6 +45,8 @@ public static <T> SingleValueColumnSource<T> getSingleValueColumnSource(Class<T>
             result = new LongSingleValueSource();
         } else if (type == Short.class || type == short.class) {
             result = new ShortSingleValueSource();
+        } else if (type == Boolean.class || type == boolean.class) {
+            result = new BooleanSingleValueSource();
         } else {
             result = new ObjectSingleValueSource<>(type);
         }
@@ -91,6 +95,13 @@ public void setNull() {
         throw new UnsupportedOperationException();
     }
 
+    @Override
+    public final void setNull(RowSequence orderedKeys) {
+        if (!orderedKeys.isEmpty()) {
+            setNull();
+        }
+    }
+
     @Override
     public final void ensureCapacity(long capacity, boolean nullFilled) {
         // Do nothing
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/WritableRedirectedColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/WritableRedirectedColumnSource.java
index edee7e4f34a..4cc083580b8 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/WritableRedirectedColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/WritableRedirectedColumnSource.java
@@ -19,7 +19,25 @@
  * {@link RowRedirection}.
  */
 public class WritableRedirectedColumnSource<T> extends RedirectedColumnSource<T> implements WritableColumnSource<T> {
-    private long maxInnerIndex;
+    /**
+     * Redirect the innerSource if it is not agnostic to redirection. Otherwise, return the innerSource.
+     *
+     * @param rowRedirection The row redirection to use
+     * @param innerSource The column source to redirect
+     * @param maxInnerIndex The maximum row key available in innerSource
+     */
+    public static <T> WritableColumnSource<T> maybeRedirect(
+            @NotNull final RowRedirection rowRedirection,
+            @NotNull final WritableColumnSource<T> innerSource,
+            final long maxInnerIndex) {
+        if (innerSource instanceof RowKeyAgnosticColumnSource) {
+            return innerSource;
+        }
+        return new WritableRedirectedColumnSource<>(rowRedirection, innerSource, maxInnerIndex);
+    }
+
+    /** The maximum row key available in innerSource. */
+    private final long maxInnerIndex;
 
     /**
      * Create a type-appropriate WritableRedirectedColumnSource for the supplied {@link WritableRowRedirection} and
@@ -29,7 +47,8 @@ public class WritableRedirectedColumnSource<T> extends RedirectedColumnSource<T>
      * @param innerSource The column source to redirect
      * @param maxInnerIndex The maximum row key available in innerSource
      */
-    public WritableRedirectedColumnSource(@NotNull final RowRedirection rowRedirection,
+    protected WritableRedirectedColumnSource(
+            @NotNull final RowRedirection rowRedirection,
             @NotNull final ColumnSource<T> innerSource,
             final long maxInnerIndex) {
         super(rowRedirection, innerSource);
@@ -140,7 +159,7 @@ public <ALTERNATE_DATA_TYPE> boolean allowsReinterpret(@NotNull Class<ALTERNATE_
     @Override
     protected <ALTERNATE_DATA_TYPE> ColumnSource<ALTERNATE_DATA_TYPE> doReinterpret(
             @NotNull Class<ALTERNATE_DATA_TYPE> alternateDataType) {
-        return new WritableRedirectedColumnSource<>(
-                rowRedirection, innerSource.reinterpret(alternateDataType), maxInnerIndex);
+        return WritableRedirectedColumnSource.maybeRedirect(rowRedirection,
+                (WritableColumnSource<ALTERNATE_DATA_TYPE>) innerSource.reinterpret(alternateDataType), maxInnerIndex);
     }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantByteSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantByteSource.java
index 05a54abd5fa..279b052a10d 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantByteSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantByteSource.java
@@ -1,3 +1,6 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
 /*
  * ---------------------------------------------------------------------------------------------------------------------
  * AUTO-GENERATED CLASS - DO NOT EDIT MANUALLY - for any changes edit ImmutableConstantCharSource and regenerate
@@ -7,9 +10,12 @@
 
 import io.deephaven.engine.table.ColumnSource;
 
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.WritableByteChunk;
 import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
 import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.RowKeys;
 import io.deephaven.engine.table.impl.AbstractColumnSource;
 import io.deephaven.engine.table.impl.ImmutableColumnSourceGetDefaults;
 import io.deephaven.engine.table.impl.sources.*;
@@ -27,7 +33,8 @@
  */
 public class ImmutableConstantByteSource
         extends AbstractColumnSource<Byte>
-        implements ImmutableColumnSourceGetDefaults.ForByte, InMemoryColumnSource, ShiftData.ShiftCallback {
+        implements ImmutableColumnSourceGetDefaults.ForByte, ShiftData.ShiftCallback,
+        RowKeyAgnosticColumnSource<Values> {
 
     private final byte value;
 
@@ -80,4 +87,26 @@ protected <ALTERNATE_DATA_TYPE> ColumnSource<ALTERNATE_DATA_TYPE> doReinterpret(
          return (ColumnSource<ALTERNATE_DATA_TYPE>) new ByteAsBooleanColumnSource(this);
     }
     // endregion reinterpret
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        final WritableByteChunk<? super Values> destChunk = dest.asWritableByteChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_BYTE : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        fillChunkUnordered(context , dest, keys);
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantCharSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantCharSource.java
index c39a3d1f530..95d8afb17b5 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantCharSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantCharSource.java
@@ -1,8 +1,14 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
 package io.deephaven.engine.table.impl.sources.immutable;
 
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.WritableCharChunk;
 import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
 import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.RowKeys;
 import io.deephaven.engine.table.impl.AbstractColumnSource;
 import io.deephaven.engine.table.impl.ImmutableColumnSourceGetDefaults;
 import io.deephaven.engine.table.impl.sources.*;
@@ -20,7 +26,8 @@
  */
 public class ImmutableConstantCharSource
         extends AbstractColumnSource<Character>
-        implements ImmutableColumnSourceGetDefaults.ForChar, InMemoryColumnSource, ShiftData.ShiftCallback {
+        implements ImmutableColumnSourceGetDefaults.ForChar, ShiftData.ShiftCallback,
+        RowKeyAgnosticColumnSource<Values> {
 
     private final char value;
 
@@ -62,4 +69,26 @@ public final void shift(final long start, final long end, final long offset) {}
 
     // region reinterpret
     // endregion reinterpret
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        final WritableCharChunk<? super Values> destChunk = dest.asWritableCharChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_CHAR : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        fillChunkUnordered(context , dest, keys);
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantDoubleSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantDoubleSource.java
index 54c21ecd4c3..72d72ba7060 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantDoubleSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantDoubleSource.java
@@ -1,3 +1,6 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
 /*
  * ---------------------------------------------------------------------------------------------------------------------
  * AUTO-GENERATED CLASS - DO NOT EDIT MANUALLY - for any changes edit ImmutableConstantCharSource and regenerate
@@ -5,9 +8,12 @@
  */
 package io.deephaven.engine.table.impl.sources.immutable;
 
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.WritableDoubleChunk;
 import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
 import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.RowKeys;
 import io.deephaven.engine.table.impl.AbstractColumnSource;
 import io.deephaven.engine.table.impl.ImmutableColumnSourceGetDefaults;
 import io.deephaven.engine.table.impl.sources.*;
@@ -25,7 +31,8 @@
  */
 public class ImmutableConstantDoubleSource
         extends AbstractColumnSource<Double>
-        implements ImmutableColumnSourceGetDefaults.ForDouble, InMemoryColumnSource, ShiftData.ShiftCallback {
+        implements ImmutableColumnSourceGetDefaults.ForDouble, ShiftData.ShiftCallback,
+        RowKeyAgnosticColumnSource<Values> {
 
     private final double value;
 
@@ -67,4 +74,26 @@ public final void shift(final long start, final long end, final long offset) {}
 
     // region reinterpret
     // endregion reinterpret
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        final WritableDoubleChunk<? super Values> destChunk = dest.asWritableDoubleChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_DOUBLE : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        fillChunkUnordered(context , dest, keys);
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantFloatSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantFloatSource.java
index f09cfe25540..06a30f4da50 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantFloatSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantFloatSource.java
@@ -1,3 +1,6 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
 /*
  * ---------------------------------------------------------------------------------------------------------------------
  * AUTO-GENERATED CLASS - DO NOT EDIT MANUALLY - for any changes edit ImmutableConstantCharSource and regenerate
@@ -5,9 +8,12 @@
  */
 package io.deephaven.engine.table.impl.sources.immutable;
 
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.WritableFloatChunk;
 import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
 import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.RowKeys;
 import io.deephaven.engine.table.impl.AbstractColumnSource;
 import io.deephaven.engine.table.impl.ImmutableColumnSourceGetDefaults;
 import io.deephaven.engine.table.impl.sources.*;
@@ -25,7 +31,8 @@
  */
 public class ImmutableConstantFloatSource
         extends AbstractColumnSource<Float>
-        implements ImmutableColumnSourceGetDefaults.ForFloat, InMemoryColumnSource, ShiftData.ShiftCallback {
+        implements ImmutableColumnSourceGetDefaults.ForFloat, ShiftData.ShiftCallback,
+        RowKeyAgnosticColumnSource<Values> {
 
     private final float value;
 
@@ -67,4 +74,26 @@ public final void shift(final long start, final long end, final long offset) {}
 
     // region reinterpret
     // endregion reinterpret
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        final WritableFloatChunk<? super Values> destChunk = dest.asWritableFloatChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_FLOAT : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        fillChunkUnordered(context , dest, keys);
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantIntSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantIntSource.java
index 3ed6604a411..83b46b7e60c 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantIntSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantIntSource.java
@@ -1,3 +1,6 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
 /*
  * ---------------------------------------------------------------------------------------------------------------------
  * AUTO-GENERATED CLASS - DO NOT EDIT MANUALLY - for any changes edit ImmutableConstantCharSource and regenerate
@@ -5,9 +8,12 @@
  */
 package io.deephaven.engine.table.impl.sources.immutable;
 
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.WritableIntChunk;
 import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
 import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.RowKeys;
 import io.deephaven.engine.table.impl.AbstractColumnSource;
 import io.deephaven.engine.table.impl.ImmutableColumnSourceGetDefaults;
 import io.deephaven.engine.table.impl.sources.*;
@@ -25,7 +31,8 @@
  */
 public class ImmutableConstantIntSource
         extends AbstractColumnSource<Integer>
-        implements ImmutableColumnSourceGetDefaults.ForInt, InMemoryColumnSource, ShiftData.ShiftCallback {
+        implements ImmutableColumnSourceGetDefaults.ForInt, ShiftData.ShiftCallback,
+        RowKeyAgnosticColumnSource<Values> {
 
     private final int value;
 
@@ -67,4 +74,26 @@ public final void shift(final long start, final long end, final long offset) {}
 
     // region reinterpret
     // endregion reinterpret
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        final WritableIntChunk<? super Values> destChunk = dest.asWritableIntChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_INT : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        fillChunkUnordered(context , dest, keys);
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantLongSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantLongSource.java
index fc269dd4014..3bd263faead 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantLongSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantLongSource.java
@@ -1,3 +1,6 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
 /*
  * ---------------------------------------------------------------------------------------------------------------------
  * AUTO-GENERATED CLASS - DO NOT EDIT MANUALLY - for any changes edit ImmutableConstantCharSource and regenerate
@@ -9,9 +12,12 @@
 
 import io.deephaven.time.DateTime;
 
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.WritableLongChunk;
 import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
 import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.RowKeys;
 import io.deephaven.engine.table.impl.AbstractColumnSource;
 import io.deephaven.engine.table.impl.ImmutableColumnSourceGetDefaults;
 import io.deephaven.engine.table.impl.sources.*;
@@ -29,7 +35,8 @@
  */
 public class ImmutableConstantLongSource
         extends AbstractColumnSource<Long>
-        implements ImmutableColumnSourceGetDefaults.ForLong, InMemoryColumnSource, ShiftData.ShiftCallback {
+        implements ImmutableColumnSourceGetDefaults.ForLong, ShiftData.ShiftCallback,
+        RowKeyAgnosticColumnSource<Values> {
 
     private final long value;
 
@@ -82,4 +89,26 @@ protected <ALTERNATE_DATA_TYPE> ColumnSource<ALTERNATE_DATA_TYPE> doReinterpret(
          return (ColumnSource<ALTERNATE_DATA_TYPE>) new LongAsDateTimeColumnSource(this);
     }
     // endregion reinterpret
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        final WritableLongChunk<? super Values> destChunk = dest.asWritableLongChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_LONG : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        fillChunkUnordered(context , dest, keys);
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantObjectSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantObjectSource.java
index d44b6df4e88..947562e4ef5 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantObjectSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantObjectSource.java
@@ -1,3 +1,6 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
 /*
  * ---------------------------------------------------------------------------------------------------------------------
  * AUTO-GENERATED CLASS - DO NOT EDIT MANUALLY - for any changes edit ImmutableConstantCharSource and regenerate
@@ -5,9 +8,12 @@
  */
 package io.deephaven.engine.table.impl.sources.immutable;
 
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.WritableObjectChunk;
 import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
 import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.RowKeys;
 import io.deephaven.engine.table.impl.AbstractColumnSource;
 import io.deephaven.engine.table.impl.ImmutableColumnSourceGetDefaults;
 import io.deephaven.engine.table.impl.sources.*;
@@ -24,7 +30,8 @@
  */
 public class ImmutableConstantObjectSource<T>
         extends AbstractColumnSource<T>
-        implements ImmutableColumnSourceGetDefaults.ForObject<T>, InMemoryColumnSource, ShiftData.ShiftCallback {
+        implements ImmutableColumnSourceGetDefaults.ForObject<T>, ShiftData.ShiftCallback,
+        RowKeyAgnosticColumnSource<Values> {
 
     private final T value;
 
@@ -66,4 +73,26 @@ public final void shift(final long start, final long end, final long offset) {}
 
     // region reinterpret
     // endregion reinterpret
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        final WritableObjectChunk<T, ? super Values> destChunk = dest.asWritableObjectChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? null : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        fillChunkUnordered(context , dest, keys);
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantShortSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantShortSource.java
index cdab3b09a31..c394ac5c2bf 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantShortSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantShortSource.java
@@ -1,3 +1,6 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
 /*
  * ---------------------------------------------------------------------------------------------------------------------
  * AUTO-GENERATED CLASS - DO NOT EDIT MANUALLY - for any changes edit ImmutableConstantCharSource and regenerate
@@ -5,9 +8,12 @@
  */
 package io.deephaven.engine.table.impl.sources.immutable;
 
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.WritableShortChunk;
 import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
 import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.RowKeys;
 import io.deephaven.engine.table.impl.AbstractColumnSource;
 import io.deephaven.engine.table.impl.ImmutableColumnSourceGetDefaults;
 import io.deephaven.engine.table.impl.sources.*;
@@ -25,7 +31,8 @@
  */
 public class ImmutableConstantShortSource
         extends AbstractColumnSource<Short>
-        implements ImmutableColumnSourceGetDefaults.ForShort, InMemoryColumnSource, ShiftData.ShiftCallback {
+        implements ImmutableColumnSourceGetDefaults.ForShort, ShiftData.ShiftCallback,
+        RowKeyAgnosticColumnSource<Values> {
 
     private final short value;
 
@@ -67,4 +74,26 @@ public final void shift(final long start, final long end, final long offset) {}
 
     // region reinterpret
     // endregion reinterpret
+
+    @Override
+    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        final WritableShortChunk<? super Values> destChunk = dest.asWritableShortChunk();
+        for (int ii = 0; ii < keys.size(); ++ii) {
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_SHORT : value);
+        }
+        destChunk.setSize(keys.size());
+    }
+
+    @Override
+    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+            @NotNull LongChunk<? extends RowKeys> keys) {
+        fillChunkUnordered(context , dest, keys);
+    }
+
+    @Override
+    public boolean providesFillUnordered() {
+        return true;
+    }
 }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseByteUpdateByOperator.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseByteUpdateByOperator.java
index 1fd0c385d46..24432a06d53 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseByteUpdateByOperator.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseByteUpdateByOperator.java
@@ -101,7 +101,7 @@ public BaseByteUpdateByOperator(@NotNull final MatchPair pair,
             // region create-dense
             this.maybeInnerSource = makeDenseSource();
             // endregion create-dense
-            this.outputSource = new WritableRedirectedColumnSource(rowRedirection, maybeInnerSource, 0);
+            this.outputSource = WritableRedirectedColumnSource.maybeRedirect(rowRedirection, maybeInnerSource, 0);
         } else {
             this.maybeInnerSource = null;
             // region create-sparse
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseCharUpdateByOperator.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseCharUpdateByOperator.java
index 0e21db78eed..adcf7d101c7 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseCharUpdateByOperator.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseCharUpdateByOperator.java
@@ -95,7 +95,7 @@ public BaseCharUpdateByOperator(@NotNull final MatchPair pair,
             // region create-dense
             this.maybeInnerSource = new CharacterArraySource();
             // endregion create-dense
-            this.outputSource = new WritableRedirectedColumnSource(rowRedirection, maybeInnerSource, 0);
+            this.outputSource = WritableRedirectedColumnSource.maybeRedirect(rowRedirection, maybeInnerSource, 0);
         } else {
             this.maybeInnerSource = null;
             // region create-sparse
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseDoubleUpdateByOperator.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseDoubleUpdateByOperator.java
index 354860af0ee..c3cbd33c0a9 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseDoubleUpdateByOperator.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseDoubleUpdateByOperator.java
@@ -98,7 +98,7 @@ public BaseDoubleUpdateByOperator(@NotNull final MatchPair pair,
         this.isRedirected = rowRedirection != null;
         if(rowRedirection != null) {
             this.maybeInnerSource = new DoubleArraySource();
-            this.outputSource = new WritableRedirectedColumnSource(rowRedirection, maybeInnerSource, 0);
+            this.outputSource = WritableRedirectedColumnSource.maybeRedirect(rowRedirection, maybeInnerSource, 0);
         } else {
             this.maybeInnerSource = null;
             this.outputSource = new DoubleSparseArraySource();
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseFloatUpdateByOperator.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseFloatUpdateByOperator.java
index 60ea52d2548..613a699f132 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseFloatUpdateByOperator.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseFloatUpdateByOperator.java
@@ -93,7 +93,7 @@ public BaseFloatUpdateByOperator(@NotNull final MatchPair pair,
         this.isRedirected = rowRedirection != null;
         if(rowRedirection != null) {
             this.maybeInnerSource = new FloatArraySource();
-            this.outputSource = new WritableRedirectedColumnSource(rowRedirection, maybeInnerSource, 0);
+            this.outputSource = WritableRedirectedColumnSource.maybeRedirect(rowRedirection, maybeInnerSource, 0);
         } else {
             this.maybeInnerSource = null;
             this.outputSource = new FloatSparseArraySource();
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseIntUpdateByOperator.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseIntUpdateByOperator.java
index 49871e2dad3..8adf21343bc 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseIntUpdateByOperator.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseIntUpdateByOperator.java
@@ -100,7 +100,7 @@ public BaseIntUpdateByOperator(@NotNull final MatchPair pair,
             // region create-dense
             this.maybeInnerSource = new IntegerArraySource();
             // endregion create-dense
-            this.outputSource = new WritableRedirectedColumnSource(rowRedirection, maybeInnerSource, 0);
+            this.outputSource = WritableRedirectedColumnSource.maybeRedirect(rowRedirection, maybeInnerSource, 0);
         } else {
             this.maybeInnerSource = null;
             // region create-sparse
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseLongUpdateByOperator.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseLongUpdateByOperator.java
index cad71cddca9..623359b1e34 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseLongUpdateByOperator.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseLongUpdateByOperator.java
@@ -100,7 +100,7 @@ public BaseLongUpdateByOperator(@NotNull final MatchPair pair,
             // region create-dense
             this.maybeInnerSource = new LongArraySource();
             // endregion create-dense
-            this.outputSource = new WritableRedirectedColumnSource(rowRedirection, maybeInnerSource, 0);
+            this.outputSource = WritableRedirectedColumnSource.maybeRedirect(rowRedirection, maybeInnerSource, 0);
         } else {
             this.maybeInnerSource = null;
             // region create-sparse
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseObjectUpdateByOperator.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseObjectUpdateByOperator.java
index 477c356fe30..a3bc08f8c3b 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseObjectUpdateByOperator.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseObjectUpdateByOperator.java
@@ -102,7 +102,7 @@ public BaseObjectUpdateByOperator(@NotNull final MatchPair pair,
             // region create-dense
             this.maybeInnerSource = new ObjectArraySource(colType);
             // endregion create-dense
-            this.outputSource = new WritableRedirectedColumnSource(rowRedirection, maybeInnerSource, 0);
+            this.outputSource = WritableRedirectedColumnSource.maybeRedirect(rowRedirection, maybeInnerSource, 0);
         } else {
             this.maybeInnerSource = null;
             // region create-sparse
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseShortUpdateByOperator.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseShortUpdateByOperator.java
index 073cf9360eb..158e19f4ecf 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseShortUpdateByOperator.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/updateby/internal/BaseShortUpdateByOperator.java
@@ -100,7 +100,7 @@ public BaseShortUpdateByOperator(@NotNull final MatchPair pair,
             // region create-dense
             this.maybeInnerSource = new ShortArraySource();
             // endregion create-dense
-            this.outputSource = new WritableRedirectedColumnSource(rowRedirection, maybeInnerSource, 0);
+            this.outputSource = WritableRedirectedColumnSource.maybeRedirect(rowRedirection, maybeInnerSource, 0);
         } else {
             this.maybeInnerSource = null;
             // region create-sparse
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/util/ColumnsToRowsTransform.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/util/ColumnsToRowsTransform.java
index b8aab4bd16c..4089e955bbd 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/util/ColumnsToRowsTransform.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/util/ColumnsToRowsTransform.java
@@ -179,7 +179,7 @@ public static Table columnsToRows(final Table source, final String labelColumn,
             }
             expandSet.add(name);
             if (crossJoinShiftState != null) {
-                resultMap.put(name, new BitShiftingColumnSource<>(crossJoinShiftState, cs));
+                resultMap.put(name, BitShiftingColumnSource.maybeWrap(crossJoinShiftState, cs));
             } else {
                 resultMap.put(name, cs);
             }
diff --git a/engine/table/src/test/java/io/deephaven/engine/table/impl/QueryTableNaturalJoinTest.java b/engine/table/src/test/java/io/deephaven/engine/table/impl/QueryTableNaturalJoinTest.java
index e5496b1fe67..3e59a74c830 100644
--- a/engine/table/src/test/java/io/deephaven/engine/table/impl/QueryTableNaturalJoinTest.java
+++ b/engine/table/src/test/java/io/deephaven/engine/table/impl/QueryTableNaturalJoinTest.java
@@ -4,13 +4,16 @@
 package io.deephaven.engine.table.impl;
 
 import io.deephaven.base.FileUtils;
+import io.deephaven.chunk.ObjectChunk;
 import io.deephaven.datastructures.util.CollectionUtil;
 import io.deephaven.engine.rowset.RowSet;
 import io.deephaven.engine.rowset.RowSetBuilderSequential;
 import io.deephaven.engine.rowset.RowSetFactory;
 import io.deephaven.engine.rowset.TrackingRowSet;
+import io.deephaven.engine.table.ChunkSource;
 import io.deephaven.engine.table.ColumnDefinition;
 import io.deephaven.engine.table.ColumnSource;
+import io.deephaven.engine.table.DataColumn;
 import io.deephaven.engine.table.Table;
 import io.deephaven.engine.table.TableDefinition;
 import io.deephaven.engine.table.impl.indexer.RowSetIndexer;
@@ -1576,6 +1579,25 @@ public void testDHC3202_v2() {
         }
     }
 
+    public void testGetDirectAfterNaturalJoin() {
+        final Table sodiumLeft = emptyTable(3).updateView("Value=(i%5==0? null : i*2)", "ColLeft=`LeftOnlyContents`");
+        final Table peppermintRight =
+                emptyTable(4).updateView("Value=(i%5==0? null : i)", "ColRight=`RightOnlyContents`");
+        final Table vanillaVanilla = sodiumLeft.naturalJoin(peppermintRight, "Value");
+        final String rightValue = "RightOnlyContents";
+
+        final ColumnSource<?> colRightSource = vanillaVanilla.getColumnSource("ColRight");
+        try (final ChunkSource.GetContext gc = colRightSource.makeGetContext(3)) {
+            final ObjectChunk<String, ?> ck = colRightSource.getChunk(gc, vanillaVanilla.getRowSet()).asObjectChunk();
+            assertEquals(rightValue, ck.get(0));
+            assertEquals(rightValue, ck.get(1));
+            assertNull(ck.get(2));
+        }
+        final DataColumn<?> colRight = vanillaVanilla.getColumn("ColRight");
+        assertEquals(rightValue, colRight.get(0));
+        assertEquals(rightValue, colRight.get(1));
+        assertNull(colRight.get(2));
+    }
 
     private void diskBackedTestHarness(BiConsumer<Table, Table> testFunction) throws IOException {
         final File leftDirectory = Files.createTempDirectory("QueryTableJoinTest-Left").toFile();
diff --git a/engine/table/src/test/java/io/deephaven/engine/table/impl/StreamTableAggregationTest.java b/engine/table/src/test/java/io/deephaven/engine/table/impl/StreamTableAggregationTest.java
index 94ca3417c60..6600b19d6bb 100644
--- a/engine/table/src/test/java/io/deephaven/engine/table/impl/StreamTableAggregationTest.java
+++ b/engine/table/src/test/java/io/deephaven/engine/table/impl/StreamTableAggregationTest.java
@@ -80,7 +80,7 @@ private void doOperatorTest(@NotNull final UnaryOperator<Table> operator, final
                     new WrappedRowSetWritableRowRedirection(streamInternalRowSet);
             streamSources = source.getColumnSourceMap().entrySet().stream().collect(Collectors.toMap(
                     Map.Entry::getKey,
-                    (entry -> new RedirectedColumnSource<>(streamRedirections, entry.getValue())),
+                    (entry -> RedirectedColumnSource.maybeRedirect(streamRedirections, entry.getValue())),
                     Assert::neverInvoked,
                     LinkedHashMap::new));
         }
diff --git a/engine/table/src/test/java/io/deephaven/engine/table/impl/StreamTableOperationsTest.java b/engine/table/src/test/java/io/deephaven/engine/table/impl/StreamTableOperationsTest.java
index 1f049210332..8cf1723cb5e 100644
--- a/engine/table/src/test/java/io/deephaven/engine/table/impl/StreamTableOperationsTest.java
+++ b/engine/table/src/test/java/io/deephaven/engine/table/impl/StreamTableOperationsTest.java
@@ -79,7 +79,7 @@ private void doOperatorTest(@NotNull final UnaryOperator<Table> operator, final
                     new WrappedRowSetWritableRowRedirection(streamInternalRowSet);
             streamSources = source.getColumnSourceMap().entrySet().stream().collect(Collectors.toMap(
                     Map.Entry::getKey,
-                    (entry -> new RedirectedColumnSource<>(streamRedirections, entry.getValue())),
+                    (entry -> RedirectedColumnSource.maybeRedirect(streamRedirections, entry.getValue())),
                     Assert::neverInvoked,
                     LinkedHashMap::new));
         }
diff --git a/extensions/barrage/src/main/java/io/deephaven/extensions/barrage/table/BarrageTable.java b/extensions/barrage/src/main/java/io/deephaven/extensions/barrage/table/BarrageTable.java
index 3a9d673c3ec..00d50e978a5 100644
--- a/extensions/barrage/src/main/java/io/deephaven/extensions/barrage/table/BarrageTable.java
+++ b/extensions/barrage/src/main/java/io/deephaven/extensions/barrage/table/BarrageTable.java
@@ -428,7 +428,7 @@ protected static LinkedHashMap<String, ColumnSource<?>> makeColumns(
             writableSources[ii] = ArrayBackedColumnSource.getMemoryColumnSource(
                     0, column.getDataType(), column.getComponentType());
             finalColumns.put(column.getName(),
-                    new WritableRedirectedColumnSource<>(emptyRowRedirection, writableSources[ii], 0));
+                    WritableRedirectedColumnSource.maybeRedirect(emptyRowRedirection, writableSources[ii], 0));
         }
         return finalColumns;
     }
diff --git a/replication/static/src/main/java/io/deephaven/replicators/ReplicateSourcesAndChunks.java b/replication/static/src/main/java/io/deephaven/replicators/ReplicateSourcesAndChunks.java
index 2a2ca06acf1..6b47485a225 100644
--- a/replication/static/src/main/java/io/deephaven/replicators/ReplicateSourcesAndChunks.java
+++ b/replication/static/src/main/java/io/deephaven/replicators/ReplicateSourcesAndChunks.java
@@ -160,11 +160,16 @@ private static void replicateObjectSingleValue() throws IOException {
                 "Object current", "T current",
                 "Object prev", "T prev",
                 "ColumnSource<[?] extends Object>", "ColumnSource<? extends T>",
+                "getObject", "get",
+                "getPrevObject", "getPrev",
                 "set\\(Object", "set(T",
                 "set\\(long key, Object", "set(long key, T",
                 "set\\(NULL_OBJECT", "set(null",
                 "final ObjectChunk<[?] extends Values>", "final ObjectChunk<T, ? extends Values>",
-                "unbox\\((.*)\\)", "$1");
+                "unbox\\((.*)\\)", "$1",
+                "NULL_OBJECT", "null",
+                "WritableObjectChunk<[?] super Values>", "WritableObjectChunk<T, ? super Values>",
+                "Object value", "T value");
         lines = ReplicationUtils.removeRegion(lines, "UnboxedSetter");
         lines = ReplicationUtils.replaceRegion(lines, "Constructor", Arrays.asList(
                 "    public ObjectSingleValueSource(Class<T> type) {",

From 95b56fcebea82b186f4420bd301bad82c2b3ee62 Mon Sep 17 00:00:00 2001
From: Nathaniel Bauernfeind <natebauernfeind@deephaven.io>
Date: Thu, 19 Jan 2023 10:48:56 -0700
Subject: [PATCH 2/5] Add JavaDoc; add BooleanSingleValueSource to Replication

---
 .../sources/BooleanSingleValueSource.java     | 109 ++++++++++--------
 .../table/impl/sources/FillUnordered.java     |  18 +++
 .../ReplicateSourcesAndChunks.java            |  31 +++++
 3 files changed, 110 insertions(+), 48 deletions(-)

diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BooleanSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BooleanSingleValueSource.java
index f39f1ee3925..6956b5b0f99 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BooleanSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BooleanSingleValueSource.java
@@ -1,55 +1,48 @@
 /**
- * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ * Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
+ */
+/*
+ * ---------------------------------------------------------------------------------------------------------------------
+ * AUTO-GENERATED CLASS - DO NOT EDIT MANUALLY - for any changes edit CharacterSingleValueSource and regenerate
+ * ---------------------------------------------------------------------------------------------------------------------
  */
 package io.deephaven.engine.table.impl.sources;
 
-import io.deephaven.chunk.Chunk;
-import io.deephaven.chunk.LongChunk;
 import io.deephaven.chunk.ObjectChunk;
-import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.WritableObjectChunk;
+
+import io.deephaven.chunk.WritableChunk;
 import io.deephaven.chunk.attributes.Values;
-import io.deephaven.engine.rowset.RowSequence;
-import io.deephaven.engine.rowset.chunkattributes.RowKeys;
 import io.deephaven.engine.table.impl.MutableColumnSourceGetDefaults;
 import io.deephaven.engine.updategraph.LogicalClock;
-import io.deephaven.util.QueryConstants;
+import io.deephaven.engine.rowset.chunkattributes.RowKeys;
+import io.deephaven.chunk.Chunk;
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.engine.rowset.RowSequence;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+
+import static io.deephaven.util.QueryConstants.NULL_BOOLEAN;
 
 /**
  * Single value source for Boolean.
+ * <p>
+ * The C-haracterSingleValueSource is replicated to all other types with
+ * io.deephaven.engine.table.impl.sources.Replicate.
+ *
+ * (C-haracter is deliberately spelled that way in order to prevent Replicate from altering this very comment).
  */
 public class BooleanSingleValueSource extends SingleValueColumnSource<Boolean> implements MutableColumnSourceGetDefaults.ForBoolean {
+
     private Boolean current;
     private transient Boolean prev;
 
-    BooleanSingleValueSource() {
+    // region Constructor
+    public BooleanSingleValueSource() {
         super(Boolean.class);
-        current = QueryConstants.NULL_BOOLEAN;
-        prev = QueryConstants.NULL_BOOLEAN;
-    }
-
-    @Nullable
-    @Override
-    public Boolean get(long rowKey) {
-        if (rowKey == RowSequence.NULL_ROW_KEY) {
-            return QueryConstants.NULL_BOOLEAN;
-        }
-        return current;
-    }
-
-    @Nullable
-    @Override
-    public Boolean getPrev(long rowKey) {
-        if (rowKey == RowSequence.NULL_ROW_KEY) {
-            return QueryConstants.NULL_BOOLEAN;
-        }
-        if (!isTrackingPrevValues || changeTime < LogicalClock.DEFAULT.currentStep()) {
-            return current;
-        }
-        return prev;
+        current = NULL_BOOLEAN;
+        prev = NULL_BOOLEAN;
     }
+    // endregion Constructor
 
     @Override
     public final void set(Boolean value) {
@@ -63,37 +56,57 @@ public final void set(Boolean value) {
         current = value;
     }
 
+    // region UnboxedSetter
+    // endregion UnboxedSetter
+
+    @Override
+    public final void setNull() {
+        set(NULL_BOOLEAN);
+    }
+
     @Override
     public final void set(long key, Boolean value) {
         set(value);
     }
 
     @Override
-    public void setNull(long key) {
+    public final void setNull(long key) {
         // region null set
-        set(QueryConstants.NULL_BOOLEAN);
+        set(NULL_BOOLEAN);
         // endregion null set
     }
 
     @Override
-    public final void fillFromChunk(
-            @NotNull FillFromContext context,
-            @NotNull Chunk<? extends Values> src,
-            @NotNull RowSequence orderedKeys) {
-        if (orderedKeys.isEmpty()) {
-            return;
+    public final Boolean get(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_BOOLEAN;
         }
+        return current;
+    }
 
+    @Override
+    public final Boolean getPrev(long rowKey) {
+        if (rowKey == RowSequence.NULL_ROW_KEY) {
+            return NULL_BOOLEAN;
+        }
+        if (!isTrackingPrevValues || changeTime < LogicalClock.DEFAULT.currentStep()) {
+            return current;
+        }
+        return prev;
+    }
+
+    @Override
+    public final void fillFromChunk(@NotNull FillFromContext context, @NotNull Chunk<? extends Values> src, @NotNull RowSequence rowSequence) {
+        if (rowSequence.size() == 0) {
+            return;
+        }
         // We can only hold one value anyway, so arbitrarily take the first value in the chunk and ignore the rest.
         final ObjectChunk<Boolean, ? extends Values> chunk = src.asObjectChunk();
         set(chunk.get(0));
     }
 
     @Override
-    public void fillFromChunkUnordered(
-            @NotNull FillFromContext context,
-            @NotNull Chunk<? extends Values> src,
-            @NotNull LongChunk<RowKeys> keys) {
+    public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Chunk<? extends Values> src, @NotNull LongChunk<RowKeys> keys) {
         if (keys.size() == 0) {
             return;
         }
@@ -115,7 +128,7 @@ public void fillPrevChunk(@NotNull FillContext context,
             @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
         // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         destination.setSize(rowSequence.intSize());
-        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), get(0));
+        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), getPrev(0));
     }
 
     @Override
@@ -125,7 +138,7 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
         Boolean value = get(0);
         final WritableObjectChunk<Boolean, ? super Values> destChunk = dest.asWritableObjectChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
-            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? null : value);
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_BOOLEAN : value);
         }
         destChunk.setSize(keys.size());
     }
@@ -137,7 +150,7 @@ public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull Writab
         Boolean value = getPrev(0);
         final WritableObjectChunk<Boolean, ? super Values> destChunk = dest.asWritableObjectChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
-            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? null : value);
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_BOOLEAN : value);
         }
         destChunk.setSize(keys.size());
     }
@@ -146,4 +159,4 @@ public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull Writab
     public boolean providesFillUnordered() {
         return true;
     }
-}
\ No newline at end of file
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/FillUnordered.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/FillUnordered.java
index d40eff84d42..f12a524e71b 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/FillUnordered.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/FillUnordered.java
@@ -14,7 +14,16 @@ public interface FillUnordered<ATTR extends Any> {
     /**
      * Populates a contiguous portion of the given destination chunk with data corresponding to the keys from the given
      * {@link LongChunk}.
+     * <p>
+     * It behaves as if the following code were executed:
      * 
+     * <pre>
+     * destination.setSize(keys.size());
+     * for (int ii = 0; ii < keys.size(); ++ii) {
+     *     destination.set(ii, get(keys.get(ii)));
+     * }
+     * </pre>
+     *
      * @param context A context containing all mutable/state related data used in retrieving the Chunk.
      * @param dest The chunk to be populated according to {@code keys}
      * @param keys A chunk of individual, not assumed to be ordered keys to be fetched
@@ -27,7 +36,16 @@ void fillChunkUnordered(
     /**
      * Populates a contiguous portion of the given destination chunk with prev data corresponding to the keys from the
      * given {@link LongChunk}.
+     * <p>
+     * It behaves as if the following code were executed:
      * 
+     * <pre>
+     * destination.setSize(keys.size());
+     * for (int ii = 0; ii < keys.size(); ++ii) {
+     *     destination.set(ii, getPrev(keys.get(ii)));
+     * }
+     * </pre>
+     *
      * @param context A context containing all mutable/state related data used in retrieving the Chunk.
      * @param dest The chunk to be populated according to {@code keys}
      * @param keys A chunk of individual, not assumed to be ordered keys to be fetched
diff --git a/replication/static/src/main/java/io/deephaven/replicators/ReplicateSourcesAndChunks.java b/replication/static/src/main/java/io/deephaven/replicators/ReplicateSourcesAndChunks.java
index 6b47485a225..76086c98f8e 100644
--- a/replication/static/src/main/java/io/deephaven/replicators/ReplicateSourcesAndChunks.java
+++ b/replication/static/src/main/java/io/deephaven/replicators/ReplicateSourcesAndChunks.java
@@ -139,6 +139,7 @@ private static void replicateSingleValues() throws IOException {
         charToAllButBoolean(
                 "engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CharacterSingleValueSource.java");
         replicateObjectSingleValue();
+        replicateBooleanSingleValue();
     }
 
     private static void replicateObjectSingleValue() throws IOException {
@@ -180,6 +181,36 @@ private static void replicateObjectSingleValue() throws IOException {
         FileUtils.writeLines(resultClassJavaFile, lines);
     }
 
+    private static void replicateBooleanSingleValue() throws IOException {
+        final String resultClassJavaPath = charToBoolean(
+                "engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CharacterSingleValueSource.java");
+        final File resultClassJavaFile = new File(resultClassJavaPath);
+        List<String> lines = FileUtils.readLines(resultClassJavaFile, Charset.defaultCharset());
+        lines = ReplicationUtils.addImport(lines,
+                "import io.deephaven.chunk.ObjectChunk;",
+                "import io.deephaven.chunk.WritableObjectChunk;");
+        lines = ReplicationUtils.removeImport(lines,
+                "import io.deephaven.chunk.BooleanChunk;",
+                "import io.deephaven.chunk.WritableBooleanChunk;",
+                "import static io.deephaven.util.type.TypeUtils.unbox;");
+        lines = globalReplacements(lines,
+                "boolean current", "Boolean current",
+                "boolean prev", "Boolean prev",
+                "super\\(boolean.class", "super(Boolean.class",
+                "set\\(long key, boolean", "set(long key, Boolean",
+                "getBoolean", "get",
+                "getPrevBoolean", "getPrev",
+                "boolean get", "Boolean get",
+                "boolean value", "Boolean value",
+                "final BooleanChunk<[?] extends Values>", "final ObjectChunk<Boolean, ? extends Values>",
+                "final WritableBooleanChunk<[?] super Values>", "final WritableObjectChunk<Boolean, ? super Values>",
+                "asBooleanChunk\\(", "asObjectChunk(",
+                "asWritableBooleanChunk\\(", "asWritableObjectChunk(",
+                "unbox\\((.*)\\)", "$1");
+        lines = ReplicationUtils.removeRegion(lines, "UnboxedSetter");
+        FileUtils.writeLines(resultClassJavaFile, lines);
+    }
+
     private static void replicateChunkColumnSource() throws IOException {
         charToAllButBoolean(
                 "engine/table/src/main/java/io/deephaven/engine/table/impl/sources/chunkcolumnsource/CharChunkColumnSource.java");

From a63d9831f5f1bd74d6bd5073884d7c6529582a58 Mon Sep 17 00:00:00 2001
From: Nate Bauernfeind <nate.bauernfeind@gmail.com>
Date: Thu, 19 Jan 2023 11:23:14 -0700
Subject: [PATCH 3/5] Update
 engine/table/src/main/java/io/deephaven/engine/table/impl/SortOperation.java

Co-authored-by: Ryan Caudy <rcaudy@gmail.com>
---
 .../java/io/deephaven/engine/table/impl/SortOperation.java   | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/SortOperation.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/SortOperation.java
index ec3db462d80..374e0722c0d 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/SortOperation.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/SortOperation.java
@@ -296,10 +296,9 @@ public Result<QueryTable> initialize(boolean usePrev, long beforeClock) {
      */
     public static RowRedirection getRowRedirection(@NotNull final Table sortResult) {
         for (final ColumnSource<?> columnSource : sortResult.getColumnSources()) {
-            if (!(columnSource instanceof RedirectedColumnSource)) {
-                continue;
+            if (columnSource instanceof RedirectedColumnSource) {
+                return ((RedirectedColumnSource<?>) columnSource).getRowRedirection();
             }
-            return ((RedirectedColumnSource<?>) columnSource).getRowRedirection();
         }
         return null;
     }

From e135247aafd63b26e05912c7c5a7d294e08ee6ad Mon Sep 17 00:00:00 2001
From: Nathaniel Bauernfeind <natebauernfeind@deephaven.io>
Date: Thu, 19 Jan 2023 11:41:17 -0700
Subject: [PATCH 4/5] Ryan's feedback

---
 .../impl/sources/BitMaskingColumnSource.java     |  2 +-
 .../impl/sources/BitShiftingColumnSource.java    |  2 +-
 .../impl/sources/BooleanSingleValueSource.java   | 14 +++++---------
 .../impl/sources/ByteSingleValueSource.java      | 14 +++++---------
 .../impl/sources/CharacterSingleValueSource.java | 14 +++++---------
 .../impl/sources/CrossJoinRightColumnSource.java |  3 +--
 .../impl/sources/DoubleSingleValueSource.java    | 14 +++++---------
 .../impl/sources/FloatSingleValueSource.java     | 14 +++++---------
 .../impl/sources/IntegerSingleValueSource.java   | 14 +++++---------
 .../impl/sources/LongSingleValueSource.java      | 14 +++++---------
 .../impl/sources/NullValueColumnSource.java      |  2 +-
 .../impl/sources/ObjectSingleValueSource.java    | 14 +++++---------
 .../impl/sources/RedirectedColumnSource.java     |  3 +--
 .../impl/sources/RowKeyAgnosticChunkSource.java  | 14 ++++++++++++++
 .../impl/sources/RowKeyAgnosticColumnSource.java | 16 ----------------
 .../impl/sources/ShortSingleValueSource.java     | 14 +++++---------
 .../impl/sources/SingleValueColumnSource.java    |  4 ++--
 .../sources/WritableRedirectedColumnSource.java  |  2 +-
 .../immutable/ImmutableConstantByteSource.java   | 13 ++++++++-----
 .../immutable/ImmutableConstantCharSource.java   | 13 ++++++++-----
 .../immutable/ImmutableConstantDoubleSource.java | 13 ++++++++-----
 .../immutable/ImmutableConstantFloatSource.java  | 13 ++++++++-----
 .../immutable/ImmutableConstantIntSource.java    | 13 ++++++++-----
 .../immutable/ImmutableConstantLongSource.java   | 13 ++++++++-----
 .../immutable/ImmutableConstantObjectSource.java | 13 ++++++++-----
 .../immutable/ImmutableConstantShortSource.java  | 13 ++++++++-----
 26 files changed, 131 insertions(+), 147 deletions(-)
 create mode 100644 engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RowKeyAgnosticChunkSource.java
 delete mode 100644 engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RowKeyAgnosticColumnSource.java

diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitMaskingColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitMaskingColumnSource.java
index 95643096d0e..1f967af1d33 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitMaskingColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitMaskingColumnSource.java
@@ -30,7 +30,7 @@ public class BitMaskingColumnSource<T> extends AbstractColumnSource<T> implement
     public static <T> ColumnSource<T> maybeWrap(
             final ZeroKeyCrossJoinShiftState shiftState,
             @NotNull final ColumnSource<T> innerSource) {
-        if (innerSource instanceof RowKeyAgnosticColumnSource) {
+        if (innerSource instanceof RowKeyAgnosticChunkSource) {
             return innerSource;
         }
         return new BitMaskingColumnSource<>(shiftState, innerSource);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitShiftingColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitShiftingColumnSource.java
index e461dd19361..4de026a49f9 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitShiftingColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BitShiftingColumnSource.java
@@ -33,7 +33,7 @@ public class BitShiftingColumnSource<T> extends AbstractColumnSource<T> implemen
     public static <T> ColumnSource<T> maybeWrap(
             @NotNull final CrossJoinShiftState shiftState,
             @NotNull final ColumnSource<T> innerSource) {
-        if (innerSource instanceof RowKeyAgnosticColumnSource) {
+        if (innerSource instanceof RowKeyAgnosticChunkSource) {
             return innerSource;
         }
         return new BitShiftingColumnSource<>(shiftState, innerSource);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BooleanSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BooleanSingleValueSource.java
index 6956b5b0f99..885c44a8569 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BooleanSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/BooleanSingleValueSource.java
@@ -118,27 +118,24 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
     @Override
     public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
             @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         destination.setSize(rowSequence.intSize());
-        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), get(0));
+        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), current);
     }
 
     @Override
     public void fillPrevChunk(@NotNull FillContext context,
             @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        Boolean value = getPrev(0); // avoid duplicating the current vs prev logic in getPrev
         destination.setSize(rowSequence.intSize());
-        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), getPrev(0));
+        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), value);
     }
 
     @Override
     public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        Boolean value = get(0);
         final WritableObjectChunk<Boolean, ? super Values> destChunk = dest.asWritableObjectChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
-            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_BOOLEAN : value);
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_BOOLEAN : current);
         }
         destChunk.setSize(keys.size());
     }
@@ -146,8 +143,7 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     @Override
     public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        Boolean value = getPrev(0);
+        Boolean value = getPrev(0); // avoid duplicating the current vs prev logic in getPrev
         final WritableObjectChunk<Boolean, ? super Values> destChunk = dest.asWritableObjectChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_BOOLEAN : value);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ByteSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ByteSingleValueSource.java
index c08f5404b2a..fa78a8ed06c 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ByteSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ByteSingleValueSource.java
@@ -129,27 +129,24 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
     @Override
     public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
             @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         destination.setSize(rowSequence.intSize());
-        destination.asWritableByteChunk().fillWithValue(0, rowSequence.intSize(), getByte(0));
+        destination.asWritableByteChunk().fillWithValue(0, rowSequence.intSize(), current);
     }
 
     @Override
     public void fillPrevChunk(@NotNull FillContext context,
             @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        byte value = getPrevByte(0); // avoid duplicating the current vs prev logic in getPrevByte
         destination.setSize(rowSequence.intSize());
-        destination.asWritableByteChunk().fillWithValue(0, rowSequence.intSize(), getPrevByte(0));
+        destination.asWritableByteChunk().fillWithValue(0, rowSequence.intSize(), value);
     }
 
     @Override
     public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        byte value = getByte(0);
         final WritableByteChunk<? super Values> destChunk = dest.asWritableByteChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
-            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_BYTE : value);
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_BYTE : current);
         }
         destChunk.setSize(keys.size());
     }
@@ -157,8 +154,7 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     @Override
     public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        byte value = getPrevByte(0);
+        byte value = getPrevByte(0); // avoid duplicating the current vs prev logic in getPrevByte
         final WritableByteChunk<? super Values> destChunk = dest.asWritableByteChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_BYTE : value);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CharacterSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CharacterSingleValueSource.java
index 0cde437c6b9..2fbd654e8b4 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CharacterSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CharacterSingleValueSource.java
@@ -124,27 +124,24 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
     @Override
     public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
             @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         destination.setSize(rowSequence.intSize());
-        destination.asWritableCharChunk().fillWithValue(0, rowSequence.intSize(), getChar(0));
+        destination.asWritableCharChunk().fillWithValue(0, rowSequence.intSize(), current);
     }
 
     @Override
     public void fillPrevChunk(@NotNull FillContext context,
             @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        char value = getPrevChar(0); // avoid duplicating the current vs prev logic in getPrevChar
         destination.setSize(rowSequence.intSize());
-        destination.asWritableCharChunk().fillWithValue(0, rowSequence.intSize(), getPrevChar(0));
+        destination.asWritableCharChunk().fillWithValue(0, rowSequence.intSize(), value);
     }
 
     @Override
     public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        char value = getChar(0);
         final WritableCharChunk<? super Values> destChunk = dest.asWritableCharChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
-            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_CHAR : value);
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_CHAR : current);
         }
         destChunk.setSize(keys.size());
     }
@@ -152,8 +149,7 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     @Override
     public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        char value = getPrevChar(0);
+        char value = getPrevChar(0); // avoid duplicating the current vs prev logic in getPrevChar
         final WritableCharChunk<? super Values> destChunk = dest.asWritableCharChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_CHAR : value);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CrossJoinRightColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CrossJoinRightColumnSource.java
index a6b437a0b90..f1cd6747890 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CrossJoinRightColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/CrossJoinRightColumnSource.java
@@ -29,7 +29,6 @@
 import io.deephaven.engine.rowset.RowSet;
 import io.deephaven.engine.rowset.TrackingRowSet;
 import io.deephaven.engine.table.impl.util.ChunkUtils;
-import io.deephaven.proto.backplane.grpc.NullValue;
 import org.apache.commons.lang3.mutable.MutableInt;
 import org.apache.commons.lang3.mutable.MutableLong;
 import org.jetbrains.annotations.NotNull;
@@ -50,7 +49,7 @@ public static <T> ColumnSource<T> maybeWrap(
             @NotNull final ColumnSource<T> innerSource,
             boolean rightIsLive) {
         // Force wrapping if this is a leftOuterJoin or else we will not see the nulls; unless every row is null.
-        if ((!crossJoinManager.leftOuterJoin() && innerSource instanceof RowKeyAgnosticColumnSource)
+        if ((!crossJoinManager.leftOuterJoin() && innerSource instanceof RowKeyAgnosticChunkSource)
                 || innerSource instanceof NullValueColumnSource) {
             return innerSource;
         }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/DoubleSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/DoubleSingleValueSource.java
index bff59af4298..8ac199e2820 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/DoubleSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/DoubleSingleValueSource.java
@@ -129,27 +129,24 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
     @Override
     public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
             @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         destination.setSize(rowSequence.intSize());
-        destination.asWritableDoubleChunk().fillWithValue(0, rowSequence.intSize(), getDouble(0));
+        destination.asWritableDoubleChunk().fillWithValue(0, rowSequence.intSize(), current);
     }
 
     @Override
     public void fillPrevChunk(@NotNull FillContext context,
             @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        double value = getPrevDouble(0); // avoid duplicating the current vs prev logic in getPrevDouble
         destination.setSize(rowSequence.intSize());
-        destination.asWritableDoubleChunk().fillWithValue(0, rowSequence.intSize(), getPrevDouble(0));
+        destination.asWritableDoubleChunk().fillWithValue(0, rowSequence.intSize(), value);
     }
 
     @Override
     public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        double value = getDouble(0);
         final WritableDoubleChunk<? super Values> destChunk = dest.asWritableDoubleChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
-            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_DOUBLE : value);
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_DOUBLE : current);
         }
         destChunk.setSize(keys.size());
     }
@@ -157,8 +154,7 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     @Override
     public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        double value = getPrevDouble(0);
+        double value = getPrevDouble(0); // avoid duplicating the current vs prev logic in getPrevDouble
         final WritableDoubleChunk<? super Values> destChunk = dest.asWritableDoubleChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_DOUBLE : value);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/FloatSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/FloatSingleValueSource.java
index 9d10fb61b1e..aeb0d13293d 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/FloatSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/FloatSingleValueSource.java
@@ -129,27 +129,24 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
     @Override
     public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
             @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         destination.setSize(rowSequence.intSize());
-        destination.asWritableFloatChunk().fillWithValue(0, rowSequence.intSize(), getFloat(0));
+        destination.asWritableFloatChunk().fillWithValue(0, rowSequence.intSize(), current);
     }
 
     @Override
     public void fillPrevChunk(@NotNull FillContext context,
             @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        float value = getPrevFloat(0); // avoid duplicating the current vs prev logic in getPrevFloat
         destination.setSize(rowSequence.intSize());
-        destination.asWritableFloatChunk().fillWithValue(0, rowSequence.intSize(), getPrevFloat(0));
+        destination.asWritableFloatChunk().fillWithValue(0, rowSequence.intSize(), value);
     }
 
     @Override
     public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        float value = getFloat(0);
         final WritableFloatChunk<? super Values> destChunk = dest.asWritableFloatChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
-            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_FLOAT : value);
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_FLOAT : current);
         }
         destChunk.setSize(keys.size());
     }
@@ -157,8 +154,7 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     @Override
     public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        float value = getPrevFloat(0);
+        float value = getPrevFloat(0); // avoid duplicating the current vs prev logic in getPrevFloat
         final WritableFloatChunk<? super Values> destChunk = dest.asWritableFloatChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_FLOAT : value);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/IntegerSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/IntegerSingleValueSource.java
index 21ded020289..ab48582e797 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/IntegerSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/IntegerSingleValueSource.java
@@ -129,27 +129,24 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
     @Override
     public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
             @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         destination.setSize(rowSequence.intSize());
-        destination.asWritableIntChunk().fillWithValue(0, rowSequence.intSize(), getInt(0));
+        destination.asWritableIntChunk().fillWithValue(0, rowSequence.intSize(), current);
     }
 
     @Override
     public void fillPrevChunk(@NotNull FillContext context,
             @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        int value = getPrevInt(0); // avoid duplicating the current vs prev logic in getPrevInt
         destination.setSize(rowSequence.intSize());
-        destination.asWritableIntChunk().fillWithValue(0, rowSequence.intSize(), getPrevInt(0));
+        destination.asWritableIntChunk().fillWithValue(0, rowSequence.intSize(), value);
     }
 
     @Override
     public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        int value = getInt(0);
         final WritableIntChunk<? super Values> destChunk = dest.asWritableIntChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
-            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_INT : value);
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_INT : current);
         }
         destChunk.setSize(keys.size());
     }
@@ -157,8 +154,7 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     @Override
     public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        int value = getPrevInt(0);
+        int value = getPrevInt(0); // avoid duplicating the current vs prev logic in getPrevInt
         final WritableIntChunk<? super Values> destChunk = dest.asWritableIntChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_INT : value);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/LongSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/LongSingleValueSource.java
index ab7df16511e..57907e9b332 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/LongSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/LongSingleValueSource.java
@@ -129,27 +129,24 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
     @Override
     public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
             @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         destination.setSize(rowSequence.intSize());
-        destination.asWritableLongChunk().fillWithValue(0, rowSequence.intSize(), getLong(0));
+        destination.asWritableLongChunk().fillWithValue(0, rowSequence.intSize(), current);
     }
 
     @Override
     public void fillPrevChunk(@NotNull FillContext context,
             @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        long value = getPrevLong(0); // avoid duplicating the current vs prev logic in getPrevLong
         destination.setSize(rowSequence.intSize());
-        destination.asWritableLongChunk().fillWithValue(0, rowSequence.intSize(), getPrevLong(0));
+        destination.asWritableLongChunk().fillWithValue(0, rowSequence.intSize(), value);
     }
 
     @Override
     public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        long value = getLong(0);
         final WritableLongChunk<? super Values> destChunk = dest.asWritableLongChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
-            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_LONG : value);
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_LONG : current);
         }
         destChunk.setSize(keys.size());
     }
@@ -157,8 +154,7 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     @Override
     public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        long value = getPrevLong(0);
+        long value = getPrevLong(0); // avoid duplicating the current vs prev logic in getPrevLong
         final WritableLongChunk<? super Values> destChunk = dest.asWritableLongChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_LONG : value);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/NullValueColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/NullValueColumnSource.java
index 003042f2779..da10ae46990 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/NullValueColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/NullValueColumnSource.java
@@ -28,7 +28,7 @@
  * A column source that returns null for all keys.
  */
 public class NullValueColumnSource<T> extends AbstractColumnSource<T>
-        implements ShiftData.ShiftCallback, RowKeyAgnosticColumnSource<Values> {
+        implements ShiftData.ShiftCallback, InMemoryColumnSource, RowKeyAgnosticChunkSource<Values> {
     private static final KeyedObjectKey.Basic<Pair<Class<?>, Class<?>>, NullValueColumnSource<?>> KEY_TYPE =
             new KeyedObjectKey.Basic<>() {
                 @Override
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ObjectSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ObjectSingleValueSource.java
index 01e0e1e7bec..fd90fceb1a3 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ObjectSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ObjectSingleValueSource.java
@@ -117,27 +117,24 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
     @Override
     public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
             @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         destination.setSize(rowSequence.intSize());
-        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), get(0));
+        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), current);
     }
 
     @Override
     public void fillPrevChunk(@NotNull FillContext context,
             @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        T value = getPrev(0); // avoid duplicating the current vs prev logic in getPrev
         destination.setSize(rowSequence.intSize());
-        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), getPrev(0));
+        destination.asWritableObjectChunk().fillWithValue(0, rowSequence.intSize(), value);
     }
 
     @Override
     public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        T value = get(0);
         final WritableObjectChunk<T, ? super Values> destChunk = dest.asWritableObjectChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
-            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? null : value);
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? null : current);
         }
         destChunk.setSize(keys.size());
     }
@@ -145,8 +142,7 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     @Override
     public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        T value = getPrev(0);
+        T value = getPrev(0); // avoid duplicating the current vs prev logic in getPrev
         final WritableObjectChunk<T, ? super Values> destChunk = dest.asWritableObjectChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? null : value);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RedirectedColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RedirectedColumnSource.java
index da3a359c279..335d89a624b 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RedirectedColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RedirectedColumnSource.java
@@ -8,7 +8,6 @@
 import io.deephaven.engine.table.SharedContext;
 import io.deephaven.engine.table.ColumnSource;
 import io.deephaven.engine.table.Table;
-import io.deephaven.engine.table.WritableColumnSource;
 import io.deephaven.engine.table.impl.util.RowRedirection;
 import io.deephaven.util.BooleanUtils;
 import io.deephaven.engine.table.impl.join.dupexpand.DupExpandKernel;
@@ -45,7 +44,7 @@ public class RedirectedColumnSource<T> extends AbstractDeferredGroupingColumnSou
     public static <T> ColumnSource<T> maybeRedirect(
             @NotNull final RowRedirection rowRedirection,
             @NotNull final ColumnSource<T> innerSource) {
-        if (innerSource instanceof RowKeyAgnosticColumnSource) {
+        if (innerSource instanceof RowKeyAgnosticChunkSource) {
             return innerSource;
         }
         return new RedirectedColumnSource<>(rowRedirection, innerSource);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RowKeyAgnosticChunkSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RowKeyAgnosticChunkSource.java
new file mode 100644
index 00000000000..edfd6d0ca2a
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RowKeyAgnosticChunkSource.java
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.engine.table.impl.sources;
+
+import io.deephaven.chunk.attributes.Any;
+
+/**
+ * This is a marker interface for chunk sources that are agnostic of the row key when evaluating the value for a given
+ * row key.
+ */
+public interface RowKeyAgnosticChunkSource<ATTR extends Any> extends FillUnordered<ATTR> {
+
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RowKeyAgnosticColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RowKeyAgnosticColumnSource.java
deleted file mode 100644
index 726872f4dbf..00000000000
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RowKeyAgnosticColumnSource.java
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending
- */
-package io.deephaven.engine.table.impl.sources;
-
-import io.deephaven.chunk.attributes.Any;
-
-/**
- * This is a marker interface for column sources that are agnostic when fulfilling requested row keys.
- *
- * The marker extends from {@link InMemoryColumnSource} whether the column source is actually in memory or not; it would
- * be a waste to materialize the same value for all rows via select.
- */
-public interface RowKeyAgnosticColumnSource<ATTR extends Any> extends FillUnordered<ATTR>, InMemoryColumnSource {
-
-}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ShortSingleValueSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ShortSingleValueSource.java
index 8a54ec8aa43..d30619ecda4 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ShortSingleValueSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ShortSingleValueSource.java
@@ -129,27 +129,24 @@ public void fillFromChunkUnordered(@NotNull FillFromContext context, @NotNull Ch
     @Override
     public void fillChunk(@NotNull FillContext context, @NotNull WritableChunk<? super Values> destination,
             @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         destination.setSize(rowSequence.intSize());
-        destination.asWritableShortChunk().fillWithValue(0, rowSequence.intSize(), getShort(0));
+        destination.asWritableShortChunk().fillWithValue(0, rowSequence.intSize(), current);
     }
 
     @Override
     public void fillPrevChunk(@NotNull FillContext context,
             @NotNull WritableChunk<? super Values> destination, @NotNull RowSequence rowSequence) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
+        short value = getPrevShort(0); // avoid duplicating the current vs prev logic in getPrevShort
         destination.setSize(rowSequence.intSize());
-        destination.asWritableShortChunk().fillWithValue(0, rowSequence.intSize(), getPrevShort(0));
+        destination.asWritableShortChunk().fillWithValue(0, rowSequence.intSize(), value);
     }
 
     @Override
     public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        short value = getShort(0);
         final WritableShortChunk<? super Values> destChunk = dest.asWritableShortChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
-            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_SHORT : value);
+            destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_SHORT : current);
         }
         destChunk.setSize(keys.size());
     }
@@ -157,8 +154,7 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     @Override
     public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
-        short value = getPrevShort(0);
+        short value = getPrevShort(0); // avoid duplicating the current vs prev logic in getPrevShort
         final WritableShortChunk<? super Values> destChunk = dest.asWritableShortChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_SHORT : value);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/SingleValueColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/SingleValueColumnSource.java
index bf1a924604d..599b3f830d1 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/SingleValueColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/SingleValueColumnSource.java
@@ -11,8 +11,8 @@
 import io.deephaven.engine.table.impl.util.ShiftData;
 
 public abstract class SingleValueColumnSource<T> extends AbstractColumnSource<T>
-        implements WritableColumnSource<T>, ChunkSink<Values>, ShiftData.ShiftCallback,
-        RowKeyAgnosticColumnSource<Values> {
+        implements WritableColumnSource<T>, ChunkSink<Values>, ShiftData.ShiftCallback, InMemoryColumnSource,
+        RowKeyAgnosticChunkSource<Values> {
 
     protected transient long changeTime;
     protected boolean isTrackingPrevValues;
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/WritableRedirectedColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/WritableRedirectedColumnSource.java
index 4cc083580b8..df90ac9deec 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/WritableRedirectedColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/WritableRedirectedColumnSource.java
@@ -30,7 +30,7 @@ public static <T> WritableColumnSource<T> maybeRedirect(
             @NotNull final RowRedirection rowRedirection,
             @NotNull final WritableColumnSource<T> innerSource,
             final long maxInnerIndex) {
-        if (innerSource instanceof RowKeyAgnosticColumnSource) {
+        if (innerSource instanceof RowKeyAgnosticChunkSource) {
             return innerSource;
         }
         return new WritableRedirectedColumnSource<>(rowRedirection, innerSource, maxInnerIndex);
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantByteSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantByteSource.java
index 279b052a10d..5fb13e6f403 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantByteSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantByteSource.java
@@ -33,8 +33,8 @@
  */
 public class ImmutableConstantByteSource
         extends AbstractColumnSource<Byte>
-        implements ImmutableColumnSourceGetDefaults.ForByte, ShiftData.ShiftCallback,
-        RowKeyAgnosticColumnSource<Values> {
+        implements ImmutableColumnSourceGetDefaults.ForByte, ShiftData.ShiftCallback, InMemoryColumnSource,
+        RowKeyAgnosticChunkSource<Values> {
 
     private final byte value;
 
@@ -89,9 +89,10 @@ protected <ALTERNATE_DATA_TYPE> ColumnSource<ALTERNATE_DATA_TYPE> doReinterpret(
     // endregion reinterpret
 
     @Override
-    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         final WritableByteChunk<? super Values> destChunk = dest.asWritableByteChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_BYTE : value);
@@ -100,7 +101,9 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     }
 
     @Override
-    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillPrevChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
         fillChunkUnordered(context , dest, keys);
     }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantCharSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantCharSource.java
index 95d8afb17b5..dee2302e1c8 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantCharSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantCharSource.java
@@ -26,8 +26,8 @@
  */
 public class ImmutableConstantCharSource
         extends AbstractColumnSource<Character>
-        implements ImmutableColumnSourceGetDefaults.ForChar, ShiftData.ShiftCallback,
-        RowKeyAgnosticColumnSource<Values> {
+        implements ImmutableColumnSourceGetDefaults.ForChar, ShiftData.ShiftCallback, InMemoryColumnSource,
+        RowKeyAgnosticChunkSource<Values> {
 
     private final char value;
 
@@ -71,9 +71,10 @@ public final void shift(final long start, final long end, final long offset) {}
     // endregion reinterpret
 
     @Override
-    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         final WritableCharChunk<? super Values> destChunk = dest.asWritableCharChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_CHAR : value);
@@ -82,7 +83,9 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     }
 
     @Override
-    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillPrevChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
         fillChunkUnordered(context , dest, keys);
     }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantDoubleSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantDoubleSource.java
index 72d72ba7060..c8192949f45 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantDoubleSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantDoubleSource.java
@@ -31,8 +31,8 @@
  */
 public class ImmutableConstantDoubleSource
         extends AbstractColumnSource<Double>
-        implements ImmutableColumnSourceGetDefaults.ForDouble, ShiftData.ShiftCallback,
-        RowKeyAgnosticColumnSource<Values> {
+        implements ImmutableColumnSourceGetDefaults.ForDouble, ShiftData.ShiftCallback, InMemoryColumnSource,
+        RowKeyAgnosticChunkSource<Values> {
 
     private final double value;
 
@@ -76,9 +76,10 @@ public final void shift(final long start, final long end, final long offset) {}
     // endregion reinterpret
 
     @Override
-    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         final WritableDoubleChunk<? super Values> destChunk = dest.asWritableDoubleChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_DOUBLE : value);
@@ -87,7 +88,9 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     }
 
     @Override
-    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillPrevChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
         fillChunkUnordered(context , dest, keys);
     }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantFloatSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantFloatSource.java
index 06a30f4da50..67e063c645a 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantFloatSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantFloatSource.java
@@ -31,8 +31,8 @@
  */
 public class ImmutableConstantFloatSource
         extends AbstractColumnSource<Float>
-        implements ImmutableColumnSourceGetDefaults.ForFloat, ShiftData.ShiftCallback,
-        RowKeyAgnosticColumnSource<Values> {
+        implements ImmutableColumnSourceGetDefaults.ForFloat, ShiftData.ShiftCallback, InMemoryColumnSource,
+        RowKeyAgnosticChunkSource<Values> {
 
     private final float value;
 
@@ -76,9 +76,10 @@ public final void shift(final long start, final long end, final long offset) {}
     // endregion reinterpret
 
     @Override
-    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         final WritableFloatChunk<? super Values> destChunk = dest.asWritableFloatChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_FLOAT : value);
@@ -87,7 +88,9 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     }
 
     @Override
-    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillPrevChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
         fillChunkUnordered(context , dest, keys);
     }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantIntSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantIntSource.java
index 83b46b7e60c..8c9d8826f8a 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantIntSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantIntSource.java
@@ -31,8 +31,8 @@
  */
 public class ImmutableConstantIntSource
         extends AbstractColumnSource<Integer>
-        implements ImmutableColumnSourceGetDefaults.ForInt, ShiftData.ShiftCallback,
-        RowKeyAgnosticColumnSource<Values> {
+        implements ImmutableColumnSourceGetDefaults.ForInt, ShiftData.ShiftCallback, InMemoryColumnSource,
+        RowKeyAgnosticChunkSource<Values> {
 
     private final int value;
 
@@ -76,9 +76,10 @@ public final void shift(final long start, final long end, final long offset) {}
     // endregion reinterpret
 
     @Override
-    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         final WritableIntChunk<? super Values> destChunk = dest.asWritableIntChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_INT : value);
@@ -87,7 +88,9 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     }
 
     @Override
-    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillPrevChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
         fillChunkUnordered(context , dest, keys);
     }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantLongSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantLongSource.java
index 3bd263faead..361c45c9805 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantLongSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantLongSource.java
@@ -35,8 +35,8 @@
  */
 public class ImmutableConstantLongSource
         extends AbstractColumnSource<Long>
-        implements ImmutableColumnSourceGetDefaults.ForLong, ShiftData.ShiftCallback,
-        RowKeyAgnosticColumnSource<Values> {
+        implements ImmutableColumnSourceGetDefaults.ForLong, ShiftData.ShiftCallback, InMemoryColumnSource,
+        RowKeyAgnosticChunkSource<Values> {
 
     private final long value;
 
@@ -91,9 +91,10 @@ protected <ALTERNATE_DATA_TYPE> ColumnSource<ALTERNATE_DATA_TYPE> doReinterpret(
     // endregion reinterpret
 
     @Override
-    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         final WritableLongChunk<? super Values> destChunk = dest.asWritableLongChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_LONG : value);
@@ -102,7 +103,9 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     }
 
     @Override
-    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillPrevChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
         fillChunkUnordered(context , dest, keys);
     }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantObjectSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantObjectSource.java
index 947562e4ef5..872874053b8 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantObjectSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantObjectSource.java
@@ -30,8 +30,8 @@
  */
 public class ImmutableConstantObjectSource<T>
         extends AbstractColumnSource<T>
-        implements ImmutableColumnSourceGetDefaults.ForObject<T>, ShiftData.ShiftCallback,
-        RowKeyAgnosticColumnSource<Values> {
+        implements ImmutableColumnSourceGetDefaults.ForObject<T>, ShiftData.ShiftCallback, InMemoryColumnSource,
+        RowKeyAgnosticChunkSource<Values> {
 
     private final T value;
 
@@ -75,9 +75,10 @@ public final void shift(final long start, final long end, final long offset) {}
     // endregion reinterpret
 
     @Override
-    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         final WritableObjectChunk<T, ? super Values> destChunk = dest.asWritableObjectChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? null : value);
@@ -86,7 +87,9 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     }
 
     @Override
-    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillPrevChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
         fillChunkUnordered(context , dest, keys);
     }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantShortSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantShortSource.java
index c394ac5c2bf..e0a9f3c5317 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantShortSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/immutable/ImmutableConstantShortSource.java
@@ -31,8 +31,8 @@
  */
 public class ImmutableConstantShortSource
         extends AbstractColumnSource<Short>
-        implements ImmutableColumnSourceGetDefaults.ForShort, ShiftData.ShiftCallback,
-        RowKeyAgnosticColumnSource<Values> {
+        implements ImmutableColumnSourceGetDefaults.ForShort, ShiftData.ShiftCallback, InMemoryColumnSource,
+        RowKeyAgnosticChunkSource<Values> {
 
     private final short value;
 
@@ -76,9 +76,10 @@ public final void shift(final long start, final long end, final long offset) {}
     // endregion reinterpret
 
     @Override
-    public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
-        // We can only hold one value, fill the chunk with the value obtained from an arbitrarily valid rowKey
         final WritableShortChunk<? super Values> destChunk = dest.asWritableShortChunk();
         for (int ii = 0; ii < keys.size(); ++ii) {
             destChunk.set(ii, keys.get(ii) == RowSequence.NULL_ROW_KEY ? NULL_SHORT : value);
@@ -87,7 +88,9 @@ public void fillChunkUnordered(@NotNull FillContext context, @NotNull WritableCh
     }
 
     @Override
-    public void fillPrevChunkUnordered(@NotNull FillContext context, @NotNull WritableChunk<? super Values> dest,
+    public void fillPrevChunkUnordered(
+            @NotNull FillContext context,
+            @NotNull WritableChunk<? super Values> dest,
             @NotNull LongChunk<? extends RowKeys> keys) {
         fillChunkUnordered(context , dest, keys);
     }

From c54475ef849feb05b7ae848d542bfe1bcc199b85 Mon Sep 17 00:00:00 2001
From: Nathaniel Bauernfeind <natebauernfeind@deephaven.io>
Date: Thu, 19 Jan 2023 11:50:05 -0700
Subject: [PATCH 5/5] Revert RedirectedColumnSource constructor to protected
 and add

---
 .../engine/table/impl/AsOfJoinHelper.java        |  2 +-
 .../engine/table/impl/NaturalJoinHelper.java     |  2 +-
 .../impl/sources/RedirectedColumnSource.java     | 16 +++++++++++++++-
 3 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/AsOfJoinHelper.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/AsOfJoinHelper.java
index 24125ecfdf6..80469aea3e5 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/AsOfJoinHelper.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/AsOfJoinHelper.java
@@ -1530,7 +1530,7 @@ private static QueryTable makeResult(QueryTable leftTable, Table rightTable, Row
         Arrays.stream(columnsToAdd).forEach(mp -> {
             // note that we must always redirect the right-hand side, because unmatched rows will be redirected to null
             final ColumnSource<?> rightSource =
-                    new RedirectedColumnSource<>(rowRedirection, rightTable.getColumnSource(mp.rightColumn()));
+                    RedirectedColumnSource.alwaysRedirect(rowRedirection, rightTable.getColumnSource(mp.rightColumn()));
             if (refreshing) {
                 rightSource.startTrackingPrevValues();
             }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/NaturalJoinHelper.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/NaturalJoinHelper.java
index be3d3cb90e1..0907d8f72b9 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/NaturalJoinHelper.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/NaturalJoinHelper.java
@@ -442,7 +442,7 @@ private static QueryTable makeResult(@NotNull final QueryTable leftTable,
         for (MatchPair mp : columnsToAdd) {
             // note that we must always redirect the right-hand side, because unmatched rows will be redirected to null
             final ColumnSource<?> redirectedColumnSource =
-                    new RedirectedColumnSource<>(rowRedirection, rightTable.getColumnSource(mp.rightColumn()));
+                    RedirectedColumnSource.alwaysRedirect(rowRedirection, rightTable.getColumnSource(mp.rightColumn()));
             if (rightRefreshingColumns) {
                 redirectedColumnSource.startTrackingPrevValues();
             }
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RedirectedColumnSource.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RedirectedColumnSource.java
index 335d89a624b..1454a3a98ac 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RedirectedColumnSource.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/RedirectedColumnSource.java
@@ -50,11 +50,25 @@ public static <T> ColumnSource<T> maybeRedirect(
         return new RedirectedColumnSource<>(rowRedirection, innerSource);
     }
 
+    /**
+     * This factory method should be used when unmapped rows in the row redirection must be redirected to null values.
+     * For example, natural joins, left outer joins, and as-of joins must map unmatched rows to null values in
+     * right-side columns.
+     *
+     * @param rowRedirection The row redirection to use
+     * @param innerSource The column source to redirect
+     */
+    public static <T> ColumnSource<T> alwaysRedirect(
+            @NotNull final RowRedirection rowRedirection,
+            @NotNull final ColumnSource<T> innerSource) {
+        return new RedirectedColumnSource<>(rowRedirection, innerSource);
+    }
+
     protected final RowRedirection rowRedirection;
     protected final ColumnSource<T> innerSource;
     private final boolean ascendingMapping;
 
-    public RedirectedColumnSource(
+    protected RedirectedColumnSource(
             @NotNull final RowRedirection rowRedirection,
             @NotNull final ColumnSource<T> innerSource) {
         super(innerSource.getType());