+ * Join unique rows from a set of tables onto a set of common keys.
+ *
+ *
+ *
+ * The multiJoin operation collects the set of distinct keys from the input tables, then joins at most one row per key
+ * from each of the input tables onto the result. Input tables need not have a matching row for each key, but they may
+ * not have multiple matching rows for a given key.
+ *
+ *
+ *
+ * Input tables with non-matching key column names must use the {@link JoinMatch} format to map keys to the common
+ * output table key column names (e.g. "OutputKey=SourceKey"). Also, individual columns to include from input tables may
+ * be specified and optionally renamed using {@link io.deephaven.api.JoinAddition} format (e.g. "NewCol=OldColName"). If
+ * no output columns are specified then every non-key column from the input table will be included in the multi-join
+ * output table.
+ *
+ *
+ *
+ * The multiJoin operation can be thought of as a merge of the key columns, followed by a selectDistinct and then a
+ * series of iterative naturalJoin operations as follows (this example has common key column names and includes all
+ * columns from the input tables):
+ *
+ *
+ *
{@code
+ * private Table doIterativeMultiJoin(String [] keyColumns, List extends Table> inputTables) {
+ * final List
keyTables = inputTables.stream().map(t -> t.view(keyColumns)).collect(Collectors.toList());
+ * final Table base = TableTools.merge(keyTables).selectDistinct(keyColumns);
+ *
+ * Table result = base;
+ * for (int ii = 0; ii < inputTables.size(); ++ii) {
+ * result = result.naturalJoin(inputTables.get(ii), Arrays.asList(keyColumns));
+ * }
+ *
+ * return result;
+ * }
+ * }
+ *
+ */
+
+public class MultiJoinFactory {
+
+ /**
+ * Creator interface for runtime-supplied implementation.
+ */
+ public interface Creator {
+ MultiJoinTable of(@NotNull final MultiJoinInput... multiJoinInputs);
+ }
+
+ /**
+ * Creator provider to supply the implementation at runtime.
+ */
+ @FunctionalInterface
+ public interface CreatorProvider {
+ Creator get();
+ }
+
+ private static final class MultiJoinTableCreatorHolder {
+ private static final MultiJoinFactory.Creator creator =
+ ServiceLoader.load(MultiJoinFactory.CreatorProvider.class).iterator().next().get();
+ }
+
+ private static MultiJoinFactory.Creator multiJoinTableCreator() {
+ return MultiJoinTableCreatorHolder.creator;
+ }
+
+ /**
+ * Join tables that have common key column names; include all columns from the input tables.
+ *
+ *
+ * @param keys the key column pairs in the format "Result=Source" or "ColumnInBoth"
+ * @param inputTables the tables to join together
+ * @return a MultiJoinTable with one row for each key and the corresponding row in each input table
+ */
+ public static MultiJoinTable of(@NotNull final String[] keys, @NotNull final Table... inputTables) {
+ return multiJoinTableCreator().of(MultiJoinInput.from(keys, inputTables));
+ }
+
+ /**
+ * Perform a multiJoin for one or more tables; allows renaming of key column names and specifying individual input
+ * table columns to include in the final output table.
+ *
+ * @param multiJoinInputs the description of each table that contributes to the result
+ * @return a MultiJoinTable with one row for each key and the corresponding row in each input table
+ */
+ public static MultiJoinTable of(@NotNull final MultiJoinInput... multiJoinInputs) {
+ return multiJoinTableCreator().of(multiJoinInputs);
+ }
+}
diff --git a/engine/api/src/main/java/io/deephaven/engine/table/MultiJoinInput.java b/engine/api/src/main/java/io/deephaven/engine/table/MultiJoinInput.java
new file mode 100644
index 00000000000..f4c8facceb3
--- /dev/null
+++ b/engine/api/src/main/java/io/deephaven/engine/table/MultiJoinInput.java
@@ -0,0 +1,116 @@
+package io.deephaven.engine.table;
+
+import io.deephaven.annotations.SimpleStyle;
+import io.deephaven.api.JoinAddition;
+import io.deephaven.api.JoinMatch;
+import org.immutables.value.Value.Immutable;
+import org.immutables.value.Value.Parameter;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * An input to a multiJoin.
+ *
+ * The table, key columns, and columns to add are encapsulated in the join descriptor.
+ */
+@Immutable
+@SimpleStyle
+public abstract class MultiJoinInput {
+ /**
+ * Create a multiJoin table input.
+ *
+ * @param inputTable The table to include in a multiJoin
+ * @param columnsToMatch An array of {@link JoinMatch} specifying match conditions
+ * @param columnsToAdd An array of {@link JoinAddition} specifying the columns to add
+ */
+ public static MultiJoinInput of(
+ @NotNull final Table inputTable,
+ @NotNull JoinMatch[] columnsToMatch,
+ @NotNull JoinAddition[] columnsToAdd) {
+ return ImmutableMultiJoinInput.of(inputTable, columnsToMatch, columnsToAdd);
+ }
+
+ /**
+ * Create a multiJoin table input.
+ *
+ * @param inputTable The table to include in a multiJoin
+ * @param columnsToMatch A collection of {@link JoinMatch} specifying the key columns
+ * @param columnsToAdd A collection of {@link JoinAddition} specifying the columns to add
+ */
+ public static MultiJoinInput of(
+ @NotNull final Table inputTable,
+ @NotNull final Collection extends JoinMatch> columnsToMatch,
+ @NotNull final Collection extends JoinAddition> columnsToAdd) {
+ return of(inputTable, columnsToMatch.toArray(JoinMatch[]::new), columnsToAdd.toArray(JoinAddition[]::new));
+ }
+
+ /**
+ * Create a multiJoin table input.
+ *
+ * @param inputTable The table to include in a multiJoin
+ * @param columnsToMatch The key columns, in string format (e.g. "ResultKey=SourceKey" or "KeyInBoth").
+ * @param columnsToAdd The columns to add, in string format (e.g. "ResultColumn=SourceColumn" or
+ * "SourceColumnToAddWithSameName"); empty for all columns
+ */
+ public static MultiJoinInput of(
+ @NotNull final Table inputTable,
+ @NotNull final String[] columnsToMatch,
+ @NotNull final String[] columnsToAdd) {
+ return of(inputTable, JoinMatch.from(columnsToMatch), JoinAddition.from(columnsToAdd));
+ }
+
+ /**
+ * Create a multiJoin table input.
+ *
+ *
+ * @param inputTable The table to include in a multiJoin
+ * @param columnsToMatch The key columns, in string format (e.g. "ResultKey=SourceKey" or "KeyInBoth").
+ */
+ public static MultiJoinInput of(@NotNull final Table inputTable, @NotNull final String... columnsToMatch) {
+ return of(inputTable, JoinMatch.from(columnsToMatch), Collections.emptyList());
+ }
+
+ /**
+ * Create a multiJoin table input.
+ *
+ * @param inputTable The table to include in a multiJoin
+ * @param columnsToMatch A comma separated list of key columns, in string format (e.g. "ResultKey=SourceKey" or
+ * "KeyInBoth").
+ * @param columnsToAdd A comma separated list of columns to add, in string format (e.g. "ResultColumn=SourceColumn"
+ * or "SourceColumnToAddWithSameName"); empty for all columns
+ */
+ public static MultiJoinInput of(@NotNull final Table inputTable, String columnsToMatch, String columnsToAdd) {
+ return of(inputTable,
+ columnsToMatch == null || columnsToMatch.isEmpty()
+ ? Collections.emptyList()
+ : JoinMatch.from(columnsToMatch),
+ columnsToAdd == null || columnsToAdd.isEmpty()
+ ? Collections.emptyList()
+ : JoinAddition.from(columnsToAdd));
+ }
+
+ /**
+ * Create an array of {@link MultiJoinInput} with common keys; includes all non-key columns as output columns.
+ *
+ * @param keys The key columns, common to all tables
+ * @param inputTables An array of tables to include in the output
+ */
+ @NotNull
+ public static MultiJoinInput[] from(@NotNull final String[] keys, @NotNull final Table[] inputTables) {
+ return Arrays.stream(inputTables)
+ .map(t -> MultiJoinInput.of(t, keys))
+ .toArray(MultiJoinInput[]::new);
+ }
+
+ @Parameter
+ public abstract Table inputTable();
+
+ @Parameter
+ public abstract JoinMatch[] columnsToMatch();
+
+ @Parameter
+ public abstract JoinAddition[] columnsToAdd();
+}
diff --git a/engine/api/src/main/java/io/deephaven/engine/table/MultiJoinTable.java b/engine/api/src/main/java/io/deephaven/engine/table/MultiJoinTable.java
new file mode 100644
index 00000000000..80aa89ceb3c
--- /dev/null
+++ b/engine/api/src/main/java/io/deephaven/engine/table/MultiJoinTable.java
@@ -0,0 +1,19 @@
+package io.deephaven.engine.table;
+
+import java.util.Collection;
+
+public interface MultiJoinTable {
+ /**
+ * Get the output {@link Table table} from this multi-join table.
+ *
+ * @return The output {@link Table table}
+ */
+ Table table();
+
+ /**
+ * Get the key column names from this multi-join table.
+ *
+ * @return The key column names as a collection of strings
+ */
+ Collection keyColumns();
+}
diff --git a/engine/table/build.gradle b/engine/table/build.gradle
index 2af2095865d..1ebc7960d11 100644
--- a/engine/table/build.gradle
+++ b/engine/table/build.gradle
@@ -120,6 +120,8 @@ spotless {
'**/asofjoin/typed/rightincopen/gen/*.java',
'**/asofjoin/typed/staticopen/gen/*.java',
'**/updateby/hashing/typed/open/gen/*.java',
+ '**/multijoin/typed/staticopen/gen/*.java',
+ '**/multijoin/typed/incopen/gen/*.java',
'src/main/java/io/deephaven/engine/table/impl/SymbolTableCombiner.java',
'src/main/java/io/deephaven/libs/GroovyStaticImports.java',
'src/test/java/**/*Sample.java'
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/MultiJoinModifiedSlotTracker.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/MultiJoinModifiedSlotTracker.java
new file mode 100644
index 00000000000..d120a2726e6
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/MultiJoinModifiedSlotTracker.java
@@ -0,0 +1,236 @@
+package io.deephaven.engine.table.impl;
+
+
+import io.deephaven.engine.table.impl.sources.IntegerArraySource;
+import io.deephaven.engine.table.impl.sources.LongArraySource;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MultiJoinModifiedSlotTracker {
+ /** The output row that was modified. */
+ private final IntegerArraySource modifiedOutputRows = new IntegerArraySource();
+ /**
+ * We store flags, one per nibble; 16 per location in flags, contiguous for multiple tables when we have more than
+ * 16.
+ */
+ private final LongArraySource flagSource = new LongArraySource();
+ /** The original right values, parallel to modifiedSlots. */
+ private final List originalRedirection = new ArrayList<>();
+
+ int numTables = 0;
+ int flagLocationsPerSlot = 0;
+
+ /**
+ * The location that we must write to in modified slots; also if we have a pointer that falls outside the range [0,
+ * pointer); then we know it is invalid
+ */
+ private long pointer;
+ /** How many slots we have allocated */
+ private int allocated;
+ /**
+ * Each time we clear, we add an offset to our cookies, this prevents us from reading old values. Initialize to one
+ * to ensure newly allocated arrays cannot be interpreted as valid cookies.
+ */
+ private long cookieGeneration = 1;
+
+ static final long SENTINEL_UNINITIALIZED_KEY = -2;
+
+ private static final int FLAG_BITS = 4;
+ private static final int FLAGS_PER_LOCATION = 16;
+
+ public static final byte FLAG_ADD = 0x1;
+ public static final byte FLAG_REMOVE = 0x2;
+ public static final byte FLAG_MODIFY = 0x4;
+ public static final byte FLAG_SHIFT = 0x8;
+
+ /**
+ * Remove all entries from the tracker.
+ */
+ void clear() {
+ cookieGeneration += pointer;
+ if (cookieGeneration > Long.MAX_VALUE / 2) {
+ cookieGeneration = 1;
+ }
+ pointer = 0;
+ }
+
+ void ensureTableCapacity(int numTables) {
+ while (originalRedirection.size() < numTables) {
+ final LongArraySource las = new LongArraySource();
+ las.ensureCapacity(allocated);
+ originalRedirection.add(las);
+ }
+ this.numTables = numTables;
+ this.flagLocationsPerSlot = (numTables + FLAGS_PER_LOCATION - 1) / FLAGS_PER_LOCATION;
+ this.flagSource.ensureCapacity((long) allocated * flagLocationsPerSlot);
+ }
+
+ /**
+ * Is this cookie within our valid range (greater than or equal to our generation, but less than the pointer after
+ * adjustment?
+ *
+ * @param cookie The cookie to check for validity
+ *
+ * @return true if the cookie is from the current generation, and references a valid row in our table
+ */
+ private boolean isValidCookie(long cookie) {
+ return cookie >= cookieGeneration && getPointerFromCookie(cookie) < pointer;
+ }
+
+ /**
+ * Get a cookie to return to the user, given a pointer value.
+ *
+ * @param pointer The pointer to convert to a cookie
+ * @return The cookie to return to the user
+ */
+ private long getCookieFromPointer(long pointer) {
+ return cookieGeneration + pointer;
+ }
+
+ /**
+ * Given a valid user's cookie, return the corresponding pointer.
+ *
+ * @param cookie The valid cookie
+ * @return The pointer into modifiedSlots
+ */
+ private long getPointerFromCookie(long cookie) {
+ return cookie - cookieGeneration;
+ }
+
+ /**
+ * Add a slot in the tracker to mark an add/remove/shift of an output row.
+ *
+ * @param outputRow The row to mark for modifications
+ * @param originalRedirection The redirection value before our modification
+ * @param flags The flags to or into our state
+ *
+ * @return The cookie for future access
+ */
+ public long addSlot(final long cookie, final int outputRow, final int tableNumber, final long originalRedirection,
+ final byte flags) {
+ if (!isValidCookie(cookie)) {
+ // Create a new slot in the tracker and reset the flags and redirection.
+ maybeAllocateChunk();
+ initializeNextTrackerSlot(outputRow, tableNumber, flags);
+
+ for (int ii = 0; ii < this.originalRedirection.size(); ++ii) {
+ final LongArraySource originalRedirectionForTable = this.originalRedirection.get(ii);
+ // Store the original redirection for this table, reset the others.
+ if (ii == tableNumber) {
+ originalRedirectionForTable.set(pointer, originalRedirection);
+ } else {
+ originalRedirectionForTable.set(pointer, SENTINEL_UNINITIALIZED_KEY);
+ }
+ }
+ return getCookieFromPointer(pointer++);
+ } else {
+ // This tracker slot exists, update the flags and set the redirection for this row.
+ final long pointer = getPointerFromCookie(cookie);
+
+ doFlagUpdate(tableNumber, flags, pointer);
+
+ final LongArraySource originalRedirectionForTable = this.originalRedirection.get(tableNumber);
+ if (originalRedirectionForTable.getUnsafe(pointer) == SENTINEL_UNINITIALIZED_KEY) {
+ originalRedirectionForTable.set(pointer, originalRedirection);
+ }
+ return cookie;
+ }
+ }
+
+ /**
+ * Add a slot in the tracker to mark a modification to an output row.
+ *
+ * @param outputRow The slot to add to the tracker.
+ * @param flags The flags to or into our state
+ *
+ * @return The cookie for future access
+ */
+ public long modifySlot(final long cookie, final int outputRow, final int tableNumber, final byte flags) {
+ if (!isValidCookie(cookie)) {
+ // Create a new slot in the tracker and reset the flags and redirection for this row in all tables.
+ maybeAllocateChunk();
+ initializeNextTrackerSlot(outputRow, tableNumber, flags);
+ for (final LongArraySource originalRedirectionForTable : this.originalRedirection) {
+ originalRedirectionForTable.set(pointer, SENTINEL_UNINITIALIZED_KEY);
+ }
+ return getCookieFromPointer(pointer++);
+ } else {
+ // This tracker slot exists, update the flags only.
+ final long pointer = getPointerFromCookie(cookie);
+ doFlagUpdate(tableNumber, flags, pointer);
+ return cookie;
+ }
+ }
+
+ private long flagLocationForSlotAndTable(final long pointer, final int tableNumber) {
+ return pointer * flagLocationsPerSlot + (tableNumber / FLAGS_PER_LOCATION);
+ }
+
+ private int flagShiftForTable(final int tableNumber) {
+ return (tableNumber % FLAGS_PER_LOCATION) * FLAG_BITS;
+ }
+
+ private long setFlagInLong(final int tableNumber, final byte flag) {
+ return ((long) flag) << flagShiftForTable(tableNumber);
+ }
+
+ private long setFlagInLong(long existingLong, int tableNumber, byte flag) {
+ return existingLong | setFlagInLong(tableNumber, flag);
+ }
+
+ private void maybeAllocateChunk() {
+ if (pointer == allocated) {
+ allocated += JoinControl.CHUNK_SIZE;
+ modifiedOutputRows.ensureCapacity(allocated);
+ flagSource.ensureCapacity((long) allocated * flagLocationsPerSlot);
+ this.originalRedirection.forEach(las -> las.ensureCapacity(allocated));
+ }
+ }
+
+ private void initializeNextTrackerSlot(final int outputRow, final int tableNumber, final byte flags) {
+ modifiedOutputRows.set(pointer, outputRow);
+ for (int ii = 0; ii < flagLocationsPerSlot; ++ii) {
+ flagSource.set(pointer * flagLocationsPerSlot + ii, 0L);
+ }
+ flagSource.set(flagLocationForSlotAndTable(pointer, tableNumber), setFlagInLong(tableNumber, flags));
+ }
+
+ private void doFlagUpdate(int tableNumber, byte flags, long pointer) {
+ final long flagLocation = flagLocationForSlotAndTable(pointer, tableNumber);
+ final long existingFlagLong = flagSource.getUnsafe(flagLocation);
+ final long updatedFlagLong = setFlagInLong(existingFlagLong, tableNumber, flags);
+ flagSource.set(flagLocation, updatedFlagLong);
+ }
+
+ public interface ModifiedSlotConsumer {
+ void accept(int outputRow, long[] previousRedirections, byte[] flags);
+ }
+
+ void forAllModifiedSlots(ModifiedSlotConsumer slotConsumer) {
+ final long[] previousRedirections = new long[numTables];
+ final byte[] flagValues = new byte[numTables];
+ for (long ii = 0; ii < pointer; ++ii) {
+ final int outputRow = modifiedOutputRows.getInt(ii);
+
+ int tt = 0;
+ // Processes fully populated longs.
+ for (int ff = 0; ff < flagLocationsPerSlot - 1; ++ff) {
+ final long flagLong = flagSource.getUnsafe((ii * flagLocationsPerSlot) + ff);
+ for (int jj = 0; jj < FLAGS_PER_LOCATION; ++jj) {
+ flagValues[tt++] = (byte) ((flagLong >> (FLAG_BITS * jj)) & ((1L << FLAG_BITS) - 1));
+ }
+ }
+ // Processes the final long containing <= 16 flags.
+ final long flagLong = flagSource.getUnsafe((ii * flagLocationsPerSlot) + flagLocationsPerSlot - 1);
+ for (int jj = 0; tt < numTables; ++jj) {
+ flagValues[tt++] = (byte) ((flagLong >> (FLAG_BITS * jj)) & ((1L << FLAG_BITS) - 1));
+ }
+
+ for (tt = 0; tt < previousRedirections.length; ++tt) {
+ previousRedirections[tt] = originalRedirection.get(tt).getUnsafe(ii);
+ }
+ slotConsumer.accept(outputRow, previousRedirections, flagValues);
+ }
+ }
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/MultiJoinStateManager.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/MultiJoinStateManager.java
new file mode 100644
index 00000000000..bc7eb4b6e12
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/MultiJoinStateManager.java
@@ -0,0 +1,51 @@
+package io.deephaven.engine.table.impl;
+
+import io.deephaven.engine.table.ColumnSource;
+import io.deephaven.engine.table.Table;
+import io.deephaven.engine.table.impl.util.RowRedirection;
+
+/**
+ * This is a common interface for the static and incremental state manager so that our bucketed MultiJoinTable system is
+ * capable of using them interchangeably to build the table.
+ */
+public interface MultiJoinStateManager {
+ /**
+ * Add the given table to this multiJoin result.
+ *
+ * @param table the table to add
+ * @param sources the column sources that contain the keys
+ * @param tableNumber the table number for which we are adding rows
+ */
+ void build(final Table table, ColumnSource>[] sources, int tableNumber);
+
+ /**
+ * Get the number of rows in the result table
+ *
+ * @return the number of rows in the result table
+ */
+ long getResultSize();
+
+ /**
+ * Get the hash table column sources for the result table. These are used as the key columns of our result.
+ */
+ ColumnSource>[] getKeyHashTableSources();
+
+ /**
+ * Get the result {@link RowRedirection row redirection} for a given table
+ *
+ * @param tableNumber the table to fetch
+ * @return the row redirection for the table
+ */
+ RowRedirection getRowRedirectionForTable(int tableNumber);
+
+ /**
+ * Ensure that this state manager can handle {@code numTables} tables as constituents of the multiJoin.
+ *
+ * @param numTables the number of tables that participate
+ */
+ void ensureTableCapacity(int numTables);
+
+ void setTargetLoadFactor(final double targetLoadFactor);
+
+ void setMaximumLoadFactor(final double maximumLoadFactor);
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/MultiJoinTableCreatorImpl.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/MultiJoinTableCreatorImpl.java
new file mode 100644
index 00000000000..c329ecddd7b
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/MultiJoinTableCreatorImpl.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.engine.table.impl;
+
+import com.google.auto.service.AutoService;
+import io.deephaven.engine.table.MultiJoinFactory;
+import io.deephaven.engine.table.MultiJoinInput;
+import io.deephaven.engine.table.MultiJoinTable;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Engine-specific implementation of {@link MultiJoinFactory.Creator}.
+ */
+@SuppressWarnings("unused")
+public enum MultiJoinTableCreatorImpl implements MultiJoinFactory.Creator {
+ INSTANCE;
+
+ @Override
+ public MultiJoinTable of(@NotNull MultiJoinInput... multiJoinInputs) {
+ if (multiJoinInputs.length == 0) {
+ throw new IllegalArgumentException("At least one table must be included in MultiJoinTable.");
+ }
+ return MultiJoinTableImpl.of(multiJoinInputs);
+ }
+
+ @SuppressWarnings("unused")
+ @AutoService(MultiJoinFactory.CreatorProvider.class)
+ public static final class ProviderImpl implements MultiJoinFactory.CreatorProvider {
+ @Override
+ public MultiJoinFactory.Creator get() {
+ return INSTANCE;
+ }
+ }
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/MultiJoinTableImpl.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/MultiJoinTableImpl.java
new file mode 100644
index 00000000000..95f2f84cf4a
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/MultiJoinTableImpl.java
@@ -0,0 +1,710 @@
+package io.deephaven.engine.table.impl;
+
+import gnu.trove.map.hash.TObjectIntHashMap;
+import io.deephaven.api.ColumnName;
+import io.deephaven.api.JoinAddition;
+import io.deephaven.api.JoinMatch;
+import io.deephaven.engine.context.ExecutionContext;
+import io.deephaven.engine.rowset.RowSetBuilderRandom;
+import io.deephaven.engine.rowset.RowSetFactory;
+import io.deephaven.engine.rowset.RowSetShiftData;
+import io.deephaven.engine.rowset.WritableRowSet;
+import io.deephaven.engine.table.*;
+import io.deephaven.engine.table.impl.by.BitmapRandomBuilder;
+import io.deephaven.engine.table.impl.by.typed.TypedHasherFactory;
+import io.deephaven.engine.table.impl.multijoin.IncrementalMultiJoinStateManagerTypedBase;
+import io.deephaven.engine.table.impl.multijoin.StaticMultiJoinStateManagerTypedBase;
+import io.deephaven.engine.table.impl.perf.QueryPerformanceRecorder;
+import io.deephaven.engine.table.impl.sources.RedirectedColumnSource;
+import io.deephaven.engine.table.impl.sources.ReinterpretUtils;
+import io.deephaven.engine.table.impl.util.RowRedirection;
+import io.deephaven.engine.table.impl.util.SingleValueRowRedirection;
+import io.deephaven.engine.table.impl.util.WritableSingleValueRowRedirection;
+import io.deephaven.engine.updategraph.NotificationQueue;
+import io.deephaven.engine.updategraph.UpdateGraph;
+import io.deephaven.util.SafeCloseable;
+import io.deephaven.util.annotations.VisibleForTesting;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static io.deephaven.engine.rowset.RowSequence.NULL_ROW_KEY;
+import static io.deephaven.engine.table.impl.MultiJoinModifiedSlotTracker.*;
+
+public class MultiJoinTableImpl implements MultiJoinTable {
+
+ private static final int KEY_COLUMN_SENTINEL = -2;
+ private final Table table;
+
+ private final List keyColumns;
+
+ private static class MultiJoinInputHelper {
+ Table table;
+
+ /** The output column keys in the order provided by the MutiJoinInput */
+ final String[] keyColumnNames;
+ /** The input column keys in the order provided by the MutiJoinInput */
+ final String[] originalKeyColumnNames;
+ /** The output non-key columns in the order provided by the MutiJoinInput */
+ final String[] addColumnNames;
+ /** The input non-key columns in the order provided by the MutiJoinInput */
+ final String[] originalAddColumnNames;
+
+ final Map> keySourceMap;
+ final Map> originalKeySourceMap;
+
+ final JoinAddition[] columnsToAdd;
+
+ MultiJoinInputHelper(@NotNull MultiJoinInput input) {
+ table = input.inputTable().coalesce();
+
+ final int matchCount = input.columnsToMatch().length;
+
+ // Create the ordered list of input as well as the deterministic order from the hashmap.
+ keyColumnNames = new String[matchCount];
+ originalKeyColumnNames = new String[matchCount];
+
+ keySourceMap = new HashMap<>(matchCount);
+ originalKeySourceMap = new HashMap<>(matchCount);
+
+ for (int ii = 0; ii < matchCount; ii++) {
+ final JoinMatch jm = input.columnsToMatch()[ii];
+ final String left = jm.left().name();
+ final String right = jm.right().name();
+
+ keyColumnNames[ii] = left;
+ originalKeyColumnNames[ii] = right;
+
+ keySourceMap.put(left, ReinterpretUtils.maybeConvertToPrimitive(table.getColumnSource(right)));
+ originalKeySourceMap.put(left, table.getColumnSource(right));
+ }
+
+ // Create the lists of addition columns (or use every non-key column if unspecified).
+ if (input.columnsToAdd().length == 0) {
+ // Compare against the input table key column names (not output).
+ final Set keys = new HashSet<>(Arrays.asList(originalKeyColumnNames));
+ // create them on the fly from the table
+ columnsToAdd = input.inputTable().getDefinition().getColumnNames().stream()
+ .filter(cn -> !keys.contains(cn)).map(ColumnName::of)
+ .toArray(JoinAddition[]::new);
+ } else {
+ columnsToAdd = input.columnsToAdd();
+ }
+
+ addColumnNames = new String[columnsToAdd.length];
+ originalAddColumnNames = new String[columnsToAdd.length];
+ for (int ii = 0; ii < columnsToAdd.length; ii++) {
+ final JoinAddition ja = columnsToAdd[ii];
+ addColumnNames[ii] = ja.newColumn().name();
+ originalAddColumnNames[ii] = ja.existingColumn().name();
+ }
+ }
+
+ void assertCompatible(@NotNull MultiJoinTableImpl.MultiJoinInputHelper inputHelper, int tableNumber) {
+ // Verify the key column names.
+ if (!keySourceMap.keySet().equals(inputHelper.keySourceMap.keySet())) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Key column mismatch for table %d, first table has key columns=%s, this table has %s",
+ tableNumber, inputHelper.keySourceMap.keySet(), keySourceMap.keySet()));
+ }
+ // Verify matching column types.
+ final String[] keys = inputHelper.keyColumnNames;
+ final Collection> currentColumnTypes =
+ Arrays.stream(keySources(keys)).map(ColumnSource::getType).collect(Collectors.toSet());
+ final Collection> expectedColumnTypes =
+ Arrays.stream(inputHelper.keySources(keys)).map(ColumnSource::getType).collect(Collectors.toSet());
+ if (!currentColumnTypes.equals(expectedColumnTypes)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Key column type mismatch for table %d, first table has key column types=%s, this table has %s",
+ tableNumber, expectedColumnTypes, currentColumnTypes));
+ }
+ // Verify matching column component types.
+ final Collection> currentComponentTypes =
+ Arrays.stream(keySources(keys)).map(ColumnSource::getComponentType).collect(Collectors.toSet());
+ final Collection> expectedComponentTypes = Arrays.stream(inputHelper.keySources(keys))
+ .map(ColumnSource::getComponentType).collect(Collectors.toSet());
+ if (!currentComponentTypes.equals(expectedComponentTypes)) {
+ throw new IllegalArgumentException(String.format(
+ "Key column component type mismatch for table %d, first table has key column component types=%s, this table has %s",
+ tableNumber, expectedComponentTypes, currentComponentTypes));
+ }
+ }
+
+ ColumnSource>[] keySources(final String[] columns) {
+ return Arrays.stream(columns).map(keySourceMap::get).toArray(ColumnSource>[]::new);
+ }
+
+ ColumnSource>[] keySources() {
+ return keySources(keyColumnNames);
+ }
+
+ ColumnSource>[] originalKeySources(final String[] columns) {
+ return Arrays.stream(columns).map(originalKeySourceMap::get).toArray(ColumnSource>[]::new);
+ }
+
+ ColumnSource>[] originalKeySources() {
+ return originalKeySources(keyColumnNames);
+ }
+ }
+
+ @VisibleForTesting
+ static MultiJoinTableImpl of(@NotNull final JoinControl joinControl,
+ @NotNull final MultiJoinInput... multiJoinInputs) {
+
+ final Table[] tables = Arrays.stream(multiJoinInputs).map(MultiJoinInput::inputTable).toArray(Table[]::new);
+ final UpdateGraph updateGraph = tables[0].getUpdateGraph(tables);
+ if (updateGraph != null) {
+ updateGraph.checkInitiateSerialTableOperation();
+ }
+ try (final SafeCloseable ignored = ExecutionContext.getContext().withUpdateGraph(updateGraph).open()) {
+ return QueryPerformanceRecorder.withNugget("multiJoin",
+ () -> new MultiJoinTableImpl(joinControl, multiJoinInputs));
+ }
+ }
+
+ static MultiJoinTableImpl of(@NotNull final MultiJoinInput... multiJoinInputs) {
+ return of(new JoinControl(), multiJoinInputs);
+ }
+
+ /**
+ * Get the output {@link Table table} from this multi-join table.
+ *
+ * @return The output {@link Table table}
+ */
+ public Table table() {
+ return table;
+ }
+
+ @Override
+ public Collection keyColumns() {
+ return keyColumns;
+ }
+
+ private MultiJoinTableImpl(@NotNull final JoinControl joinControl,
+ @NotNull final MultiJoinInput... multiJoinInputs) {
+ keyColumns = new ArrayList<>();
+
+ // Create the join input helpers we'll use during the join creation phase.
+ final MultiJoinInputHelper[] joinInputHelpers =
+ Arrays.stream(multiJoinInputs).map(MultiJoinInputHelper::new).toArray(MultiJoinInputHelper[]::new);
+ final TObjectIntHashMap usedColumns =
+ new TObjectIntHashMap<>(joinInputHelpers[0].columnsToAdd.length, 0.5f, -1);
+
+ for (String keyColName : joinInputHelpers[0].keyColumnNames) {
+ keyColumns.add(keyColName);
+ usedColumns.put(keyColName, KEY_COLUMN_SENTINEL);
+ }
+
+ for (int ii = 1; ii < joinInputHelpers.length; ++ii) {
+ // Verify this input table is compatible with the first table.
+ joinInputHelpers[ii].assertCompatible(joinInputHelpers[0], ii);
+ }
+
+ // Verify the non-key output columns do not conflict.
+ for (int ii = 0; ii < joinInputHelpers.length; ++ii) {
+ MultiJoinInputHelper inputHelper = joinInputHelpers[ii];
+ for (String columnName : inputHelper.addColumnNames) {
+ final int previouslyUsed = usedColumns.put(columnName, ii);
+ if (previouslyUsed != usedColumns.getNoEntryValue()) {
+ throw new IllegalArgumentException(String.format("Column %s defined in table %s and table %d",
+ columnName,
+ previouslyUsed == KEY_COLUMN_SENTINEL ? "key columns" : Integer.toString(previouslyUsed),
+ ii));
+ }
+ }
+ }
+
+ if (multiJoinInputs[0].columnsToMatch().length == 0) {
+ table = doMultiJoinZeroKey(joinInputHelpers);
+ return;
+ }
+ table = bucketedMultiJoin(joinControl, joinInputHelpers);
+ }
+
+ private Table bucketedMultiJoin(@NotNull final JoinControl joinControl,
+ @NotNull final MultiJoinInputHelper[] joinInputHelpers) {
+
+ final MultiJoinStateManager stateManager;
+ final String[] firstKeyColumnNames = joinInputHelpers[0].keyColumnNames;
+
+ // If any tables are refreshing, we must use a refreshing JoinManager.
+ final boolean refreshing = Arrays.stream(joinInputHelpers).anyMatch(ih -> ih.table.isRefreshing());
+ if (refreshing) {
+ stateManager = TypedHasherFactory.make(IncrementalMultiJoinStateManagerTypedBase.class,
+ joinInputHelpers[0].keySources(),
+ joinInputHelpers[0].originalKeySources(),
+ joinControl.initialBuildSize(), joinControl.getMaximumLoadFactor(),
+ joinControl.getTargetLoadFactor());
+ } else {
+ stateManager = TypedHasherFactory.make(StaticMultiJoinStateManagerTypedBase.class,
+ joinInputHelpers[0].keySources(),
+ joinInputHelpers[0].originalKeySources(),
+ joinControl.initialBuildSize(), joinControl.getMaximumLoadFactor(),
+ joinControl.getTargetLoadFactor());
+ }
+ stateManager.setMaximumLoadFactor(joinControl.getMaximumLoadFactor());
+ stateManager.setTargetLoadFactor(joinControl.getTargetLoadFactor());
+ stateManager.ensureTableCapacity(joinInputHelpers.length);
+
+ for (int tableNumber = 0; tableNumber < joinInputHelpers.length; ++tableNumber) {
+ stateManager.build(
+ joinInputHelpers[tableNumber].table,
+ joinInputHelpers[tableNumber].keySources(firstKeyColumnNames),
+ tableNumber);
+ }
+
+ final Map> resultSources = new LinkedHashMap<>();
+
+ final ColumnSource>[] keyHashTableSources = stateManager.getKeyHashTableSources();
+ final ColumnSource>[] originalColumns = joinInputHelpers[0].originalKeySources();
+
+ // We are careful to add the output key columns in the order of the first table input.
+ for (int cc = 0; cc < keyColumns.size(); ++cc) {
+ if (originalColumns[cc].getType() != keyHashTableSources[cc].getType()) {
+ resultSources.put(keyColumns.get(cc),
+ ReinterpretUtils.convertToOriginalType(originalColumns[cc], keyHashTableSources[cc]));
+ } else {
+ resultSources.put(keyColumns.get(cc), keyHashTableSources[cc]);
+ }
+ }
+
+ for (int tableNumber = 0; tableNumber < joinInputHelpers.length; ++tableNumber) {
+ final RowRedirection rowRedirection = stateManager.getRowRedirectionForTable(tableNumber);
+ if (refreshing) {
+ ((IncrementalMultiJoinStateManagerTypedBase) stateManager)
+ .startTrackingPrevRedirectionValues(tableNumber);
+ }
+ final MultiJoinInputHelper inputHelper = joinInputHelpers[tableNumber];
+ for (final JoinAddition ja : inputHelper.columnsToAdd) {
+ resultSources.put(ja.newColumn().name(), RedirectedColumnSource.alwaysRedirect(rowRedirection,
+ inputHelper.table.getColumnSource(ja.existingColumn().name())));
+ }
+ }
+
+ final QueryTable result =
+ new QueryTable(RowSetFactory.flat(stateManager.getResultSize()).toTracking(), resultSources);
+
+ if (refreshing) {
+ final ModifiedColumnSet[] resultModifiedColumnSet = new ModifiedColumnSet[joinInputHelpers.length];
+ final List listenerRecorders = new ArrayList<>();
+
+ final MergedListener mergedListener = new MultiJoinMergedListener(
+ (IncrementalMultiJoinStateManagerTypedBase) stateManager,
+ listenerRecorders,
+ Collections.emptyList(),
+ "multiJoin(" + keyColumns + ")",
+ result,
+ resultModifiedColumnSet);
+
+ for (int ii = 0; ii < joinInputHelpers.length; ++ii) {
+ final MultiJoinInputHelper inputHelper = joinInputHelpers[ii];
+ if (inputHelper.table.isRefreshing()) {
+ final QueryTable input = (QueryTable) inputHelper.table;
+ final ColumnSource>[] keySources = inputHelper.keySources(firstKeyColumnNames);
+
+ final ModifiedColumnSet sourceKeyModifiedColumnSet =
+ input.newModifiedColumnSet(inputHelper.originalKeyColumnNames);
+ final ModifiedColumnSet sourceAdditionModifiedColumnSet =
+ input.newModifiedColumnSet(inputHelper.originalAddColumnNames);
+
+ resultModifiedColumnSet[ii] =
+ result.newModifiedColumnSet(inputHelper.addColumnNames);
+ final MatchPair[] pairs = MatchPair.fromAddition(Arrays.asList(inputHelper.columnsToAdd));
+ final ModifiedColumnSet.Transformer transformer =
+ input.newModifiedColumnSetTransformer(result, pairs);
+
+ final MultiJoinListenerRecorder listenerRecorder =
+ new MultiJoinListenerRecorder("multiJoin(" + ii + ")", input, mergedListener, keySources,
+ sourceKeyModifiedColumnSet, sourceAdditionModifiedColumnSet, transformer, ii);
+ input.addUpdateListener(listenerRecorder);
+ listenerRecorders.add(listenerRecorder);
+ }
+ }
+ result.addParentReference(mergedListener);
+ }
+
+ return result;
+ }
+
+ private static Table doMultiJoinZeroKey(@NotNull final MultiJoinInputHelper[] joinInputHelpers) {
+ final SingleValueRowRedirection[] redirections = new SingleValueRowRedirection[joinInputHelpers.length];
+ final boolean refreshing = Arrays.stream(joinInputHelpers).anyMatch(ih -> ih.table.isRefreshing());
+
+ final Map> resultSources = new LinkedHashMap<>();
+ boolean hasResults = false;
+ for (int tableNumber = 0; tableNumber < joinInputHelpers.length; ++tableNumber) {
+ final MultiJoinInputHelper inputHelper = joinInputHelpers[tableNumber];
+ final Table inputTable = inputHelper.table;
+
+ final long key;
+ if (inputTable.size() == 0) {
+ key = NULL_ROW_KEY;
+ } else if (inputTable.size() == 1) {
+ key = inputTable.getRowSet().firstRowKey();
+ hasResults = true;
+ } else {
+ throw new IllegalStateException("Duplicate rows for table " + tableNumber + " on zero-key multiJoin.");
+ }
+
+ final SingleValueRowRedirection rowRedirection;
+ if (refreshing) {
+ rowRedirection = new WritableSingleValueRowRedirection(key);
+ rowRedirection.writableSingleValueCast().startTrackingPrevValues();
+ } else {
+ rowRedirection = new SingleValueRowRedirection(key);
+ }
+ redirections[tableNumber] = rowRedirection;
+
+ for (final JoinAddition ja : inputHelper.columnsToAdd) {
+ resultSources.put(ja.newColumn().name(), RedirectedColumnSource.alwaysRedirect(rowRedirection,
+ inputTable.getColumnSource(ja.existingColumn().name())));
+ }
+ }
+
+ final QueryTable result = new QueryTable(RowSetFactory.flat(hasResults ? 1 : 0).toTracking(), resultSources);
+
+ if (refreshing) {
+ final ModifiedColumnSet[] resultModifiedColumnSet = new ModifiedColumnSet[joinInputHelpers.length];
+ final List listenerRecorders = new ArrayList<>();
+
+ final MergedListener mergedListener = new MultiJoinZeroKeyMergedListener(
+ listenerRecorders,
+ Collections.emptyList(),
+ "multiJoin()",
+ result,
+ resultModifiedColumnSet,
+ redirections);
+
+ for (int ii = 0; ii < joinInputHelpers.length; ++ii) {
+ final MultiJoinInputHelper inputHelper = joinInputHelpers[ii];
+ if (inputHelper.table.isRefreshing()) {
+ final QueryTable input = (QueryTable) inputHelper.table;
+
+ final ModifiedColumnSet sourceAdditionModifiedColumnSet =
+ input.newModifiedColumnSet(inputHelper.originalAddColumnNames);
+
+ resultModifiedColumnSet[ii] =
+ result.newModifiedColumnSet(inputHelper.addColumnNames);
+ final MatchPair[] pairs = MatchPair.fromAddition(Arrays.asList(inputHelper.columnsToAdd));
+ final ModifiedColumnSet.Transformer transformer =
+ input.newModifiedColumnSetTransformer(result, pairs);
+
+ final MultiJoinListenerRecorder listenerRecorder = new MultiJoinListenerRecorder(
+ "multiJoin(" + ii + ")", input, mergedListener, null, null, sourceAdditionModifiedColumnSet,
+ transformer, ii);
+ input.addUpdateListener(listenerRecorder);
+ listenerRecorders.add(listenerRecorder);
+ }
+ }
+ result.addParentReference(mergedListener);
+ }
+
+ return result;
+ }
+
+ private static class MultiJoinListenerRecorder extends ListenerRecorder {
+ private final ColumnSource>[] keyColumns;
+ private final ModifiedColumnSet sourceKeyModifiedColumnSet;
+ private final ModifiedColumnSet sourceAdditionModifiedColumnSet;
+ private final ModifiedColumnSet.Transformer transformer;
+ private final int tableNumber;
+
+ public MultiJoinListenerRecorder(@NotNull final String description,
+ @NotNull final QueryTable parent,
+ @NotNull final MergedListener dependent,
+ final ColumnSource>[] keyColumns,
+ final ModifiedColumnSet sourceKeyModifiedColumnSet,
+ final ModifiedColumnSet sourceAdditionModifiedColumnSet,
+ @NotNull final ModifiedColumnSet.Transformer transformer,
+ final int tableNumber) {
+ super(description, parent, dependent);
+ this.keyColumns = keyColumns;
+ this.sourceKeyModifiedColumnSet = sourceKeyModifiedColumnSet;
+ this.sourceAdditionModifiedColumnSet = sourceAdditionModifiedColumnSet;
+ this.transformer = transformer;
+ this.tableNumber = tableNumber;
+
+ setMergedListener(dependent);
+ }
+
+ @Override
+ public Table getParent() {
+ return super.getParent();
+ }
+ }
+
+ private static class MultiJoinMergedListener extends MergedListener {
+ private final IncrementalMultiJoinStateManagerTypedBase stateManager;
+ private final List recorders;
+ private final ModifiedColumnSet[] modifiedColumnSets;
+ private final MultiJoinModifiedSlotTracker slotTracker = new MultiJoinModifiedSlotTracker();
+
+ protected MultiJoinMergedListener(@NotNull final IncrementalMultiJoinStateManagerTypedBase stateManager,
+ @NotNull final List recorders,
+ @NotNull final Collection dependencies,
+ @NotNull final String listenerDescription,
+ @NotNull final QueryTable result,
+ @NotNull final ModifiedColumnSet[] modifiedColumnSets) {
+ super(recorders, dependencies, listenerDescription, result);
+ this.stateManager = stateManager;
+ this.recorders = recorders;
+ this.modifiedColumnSets = modifiedColumnSets;
+ }
+
+ @Override
+ protected void process() {
+ final int tableCount = stateManager.getTableCount();
+
+ slotTracker.clear();
+ slotTracker.ensureTableCapacity(tableCount);
+
+ final long originalSize = stateManager.getResultSize();
+
+ for (MultiJoinListenerRecorder recorder : recorders) {
+ if (recorder.recordedVariablesAreValid()) {
+ final boolean keysModified = recorder.getModified().isNonempty()
+ && recorder.getModifiedColumnSet().containsAny(recorder.sourceKeyModifiedColumnSet);
+
+ if (recorder.getRemoved().isNonempty()) {
+ stateManager.processRemoved(recorder.getRemoved(), recorder.keyColumns, recorder.tableNumber,
+ slotTracker, FLAG_REMOVE);
+ }
+ if (keysModified) {
+ stateManager.processRemoved(recorder.getModifiedPreShift(), recorder.keyColumns,
+ recorder.tableNumber, slotTracker, FLAG_MODIFY);
+ }
+
+ if (recorder.getShifted().nonempty()) {
+ try (final WritableRowSet previousToShift = recorder.getParent().getRowSet().copyPrev()) {
+ previousToShift.remove(recorder.getRemoved());
+ if (keysModified) {
+ previousToShift.remove(recorder.getModifiedPreShift());
+ }
+ stateManager.processShifts(previousToShift, recorder.getShifted(), recorder.keyColumns,
+ recorder.tableNumber,
+ slotTracker);
+ }
+ }
+
+ if (!keysModified && recorder.getModified().isNonempty()) {
+ // If none of our input columns changed, we have no modifications to pass downstream.
+ if (recorder.getModifiedColumnSet().containsAny(recorder.sourceAdditionModifiedColumnSet)) {
+ stateManager.processModified(recorder.getModified(), recorder.keyColumns,
+ recorder.tableNumber,
+ slotTracker, FLAG_MODIFY);
+ }
+ }
+
+ if (keysModified) {
+ stateManager.processAdded(recorder.getModified(), recorder.keyColumns, recorder.tableNumber,
+ slotTracker, FLAG_MODIFY);
+ }
+
+ if (recorder.getAdded().isNonempty()) {
+ stateManager.processAdded(recorder.getAdded(), recorder.keyColumns, recorder.tableNumber,
+ slotTracker, FLAG_ADD);
+ }
+ }
+ }
+
+ final long newSize = stateManager.getResultSize();
+
+ final TableUpdateImpl downstream = new TableUpdateImpl();
+
+
+ if (newSize > originalSize) {
+ downstream.added = RowSetFactory.fromRange(originalSize, newSize - 1);
+ } else {
+ downstream.added = RowSetFactory.empty();
+ }
+
+ final long[] currentRedirections = new long[tableCount];
+ final boolean[] modifiedTables = new boolean[tableCount];
+ final boolean[] modifiedTablesOnThisRow = new boolean[tableCount];
+
+ final RowSetBuilderRandom modifiedBuilder = new BitmapRandomBuilder((int) newSize);
+ final RowSetBuilderRandom emptiedBuilder = RowSetFactory.builderRandom();
+ final RowSetBuilderRandom reincarnatedBuilder = RowSetFactory.builderRandom();
+
+ downstream.modifiedColumnSet = result.getModifiedColumnSetForUpdates();
+ downstream.modifiedColumnSet.clear();
+
+ final byte notShift = (FLAG_ADD | FLAG_REMOVE | FLAG_MODIFY);
+ final byte addOrRemove = (FLAG_ADD | FLAG_REMOVE);
+
+ slotTracker.forAllModifiedSlots((row, previousRedirections, flagValues) -> {
+ if (row >= originalSize) {
+ return;
+ }
+ stateManager.getCurrentRedirections(row, currentRedirections);
+ boolean allNull = true;
+ boolean rowModified = false;
+ int numberOfOriginalNulls = 0;
+ Arrays.fill(modifiedTablesOnThisRow, false);
+ for (int tableNumber = 0; tableNumber < tableCount; ++tableNumber) {
+ if (currentRedirections[tableNumber] != NULL_ROW_KEY) {
+ allNull = false;
+ }
+ if (previousRedirections[tableNumber] == NULL_ROW_KEY) {
+ numberOfOriginalNulls++;
+ }
+ if (previousRedirections[tableNumber] == MultiJoinModifiedSlotTracker.SENTINEL_UNINITIALIZED_KEY) {
+ if (currentRedirections[tableNumber] == NULL_ROW_KEY) {
+ // This slot was previously deleted and may need to be reincarnated. This redirection
+ // previously and currently points to no row and should be considered previously null
+ // for this purpose.
+ numberOfOriginalNulls++;
+ } else {
+ rowModified |= (flagValues[tableNumber] & notShift) != 0;
+ }
+ } else {
+ // If the redirection has changed and we have done anything other than a shift, we must light
+ // up all the columns for the table as a modification. Similarly, if the row was added and
+ // deleted from the original table, then we must also light up all the columns as modified.
+ if ((flagValues[tableNumber] & addOrRemove) != 0) {
+ rowModified |= (modifiedTablesOnThisRow[tableNumber] = true);
+ } else if (currentRedirections[tableNumber] != previousRedirections[tableNumber]) {
+ rowModified |=
+ (modifiedTablesOnThisRow[tableNumber] = (flagValues[tableNumber] & notShift) != 0);
+ } else {
+ rowModified |= (flagValues[tableNumber] & notShift) != 0;
+ }
+ }
+ }
+ if (allNull) {
+ emptiedBuilder.addKey(row);
+ } else if (numberOfOriginalNulls == currentRedirections.length) {
+ reincarnatedBuilder.addKey(row);
+ } else if (rowModified) {
+ modifiedBuilder.addKey(row);
+
+ for (int ii = 0; ii < currentRedirections.length; ++ii) {
+ if (modifiedTablesOnThisRow[ii]) {
+ modifiedTables[ii] = true;
+ }
+ }
+ }
+ });
+
+ downstream.modified = modifiedBuilder.build();
+ if (downstream.modified.isEmpty()) {
+ downstream.modifiedColumnSet = ModifiedColumnSet.EMPTY;
+ } else {
+ int listenerIndex = 0;
+ for (int ii = 0; ii < modifiedTables.length; ++ii) {
+ MultiJoinListenerRecorder recorder = listenerIndex >= recorders.size()
+ ? null
+ : recorders.get(listenerIndex);
+ if (recorder == null || recorder.tableNumber != ii) {
+ // We have a static table, ignore it because it cannot modify anything.
+ continue;
+ }
+ if (modifiedTables[ii]) {
+ // If this table had any rows that moved slots, or any removed/added slots, _all_ its columns
+ // are modified.
+ downstream.modifiedColumnSet.setAll(modifiedColumnSets[ii]);
+ } else if (recorder.getModified().isNonempty()) {
+ // If we have "in-place" modifications (same row, in the same slot), we need only mark the
+ // _modified_ columns from this table modified in the downstream.
+ recorder.transformer.transform(recorder.getModifiedColumnSet(), downstream.modifiedColumnSet);
+ }
+ listenerIndex++;
+ }
+ }
+ downstream.removed = emptiedBuilder.build();
+ downstream.added.writableCast().insert(reincarnatedBuilder.build());
+
+ downstream.shifted = RowSetShiftData.EMPTY;
+
+ if (!downstream.empty()) {
+ result.getRowSet().writableCast().update(downstream.added, downstream.removed);
+ result.notifyListeners(downstream);
+ }
+ }
+ }
+
+ private static class MultiJoinZeroKeyMergedListener extends MergedListener {
+ private final List recorders;
+ private final ModifiedColumnSet[] modifiedColumnSets;
+ private final SingleValueRowRedirection[] redirections;
+
+ protected MultiJoinZeroKeyMergedListener(@NotNull final List recorders,
+ @NotNull final Collection dependencies,
+ @NotNull final String listenerDescription,
+ @NotNull final QueryTable result,
+ @NotNull final ModifiedColumnSet[] modifiedColumnSets,
+ @NotNull final SingleValueRowRedirection[] redirections) {
+ super(recorders, dependencies, listenerDescription, result);
+ this.recorders = recorders;
+ this.modifiedColumnSets = modifiedColumnSets;
+ this.redirections = redirections;
+ }
+
+ @Override
+ protected void process() {
+ final TableUpdateImpl downstream = new TableUpdateImpl();
+ downstream.modifiedColumnSet = result.getModifiedColumnSetForUpdates();
+ downstream.shifted = RowSetShiftData.EMPTY;
+
+ boolean resultModified = false;
+
+ for (MultiJoinListenerRecorder recorder : recorders) {
+ if (recorder.recordedVariablesAreValid()) {
+ if (recorder.getRemoved().isNonempty() || recorder.getAdded().isNonempty()) {
+ if (recorder.getParent().size() > 1) {
+ throw new IllegalStateException(
+ "Multiple rows in " + recorder.tableNumber + " for zero-key multiJoin.");
+ }
+ resultModified = true;
+ redirections[recorder.tableNumber].writableSingleValueCast()
+ .setValue(recorder.getParent().getRowSet().firstRowKey());
+ downstream.modifiedColumnSet.setAll(modifiedColumnSets[recorder.tableNumber]);
+ } else if (recorder.getModified().isNonempty()) {
+ // If none of our input columns changed, we have no modifications to pass downstream.
+ if (recorder.getModifiedColumnSet().containsAny(recorder.sourceAdditionModifiedColumnSet)) {
+ resultModified = true;
+ recorder.transformer.transform(recorder.getModifiedColumnSet(),
+ downstream.modifiedColumnSet);
+ }
+ redirections[recorder.tableNumber].writableSingleValueCast()
+ .setValue(recorder.getParent().getRowSet().firstRowKey());
+ } else if (recorder.getShifted().nonempty()) {
+ redirections[recorder.tableNumber].writableSingleValueCast()
+ .setValue(recorder.getParent().getRowSet().firstRowKey());
+ }
+ }
+ }
+
+ final boolean hasResults = Arrays.stream(redirections).anyMatch(rd -> rd.getValue() != NULL_ROW_KEY);
+ if (hasResults && result.size() == 0) {
+ result.getRowSet().writableCast().insert(0);
+ downstream.added = RowSetFactory.flat(1);
+ downstream.removed = RowSetFactory.empty();
+ downstream.modified = RowSetFactory.empty();
+ downstream.modifiedColumnSet = ModifiedColumnSet.EMPTY;
+ } else if (!hasResults && result.size() == 1) {
+ result.getRowSet().writableCast().remove(0);
+ downstream.added = RowSetFactory.empty();
+ downstream.removed = RowSetFactory.flat(1);
+ downstream.modified = RowSetFactory.empty();
+ downstream.modifiedColumnSet = ModifiedColumnSet.EMPTY;
+ } else {
+ downstream.added = RowSetFactory.empty();
+ downstream.removed = RowSetFactory.empty();
+ if (resultModified) {
+ downstream.modified = RowSetFactory.flat(1);
+ } else {
+ // this would be a useless update
+ return;
+ }
+ }
+
+ if (!downstream.empty()) {
+ result.notifyListeners(downstream);
+ }
+ }
+ }
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/typed/HasherConfig.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/typed/HasherConfig.java
index a20fd5ab07a..d1024c861a4 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/typed/HasherConfig.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/typed/HasherConfig.java
@@ -12,7 +12,6 @@
import java.util.ArrayList;
import java.util.List;
-import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
@@ -101,17 +100,40 @@ public ProbeSpec(String name, String stateValueName, boolean requiresRowKeyChunk
}
static class BuildSpec {
+ @FunctionalInterface
+ public interface MethodBuilder {
+ void accept(HasherConfig> config, CodeBlock.Builder builder);
+ }
+
+ @FunctionalInterface
+ public interface MethodBuilderWithChunkTypes {
+ void accept(HasherConfig> config, ChunkType[] chunkTypes, CodeBlock.Builder builder);
+ }
+
final String name;
final String stateValueName;
final boolean requiresRowKeyChunk;
final boolean allowAlternates;
final FoundMethodBuilder found;
- final BiConsumer, CodeBlock.Builder> insert;
+ final MethodBuilderWithChunkTypes insert;
final ParameterSpec[] params;
public BuildSpec(String name, String stateValueName, boolean requiresRowKeyChunk,
boolean allowAlternates, FoundMethodBuilder found,
- BiConsumer, CodeBlock.Builder> insert, ParameterSpec... params) {
+ MethodBuilder insert, ParameterSpec... params) {
+ // Convert the MethodBuilder to MethodBuilderWithChunkTypes.
+ this(name,
+ stateValueName,
+ requiresRowKeyChunk,
+ allowAlternates,
+ found,
+ (config, chunkTypes, builder) -> insert.accept(config, builder),
+ params);
+ }
+
+ public BuildSpec(String name, String stateValueName, boolean requiresRowKeyChunk,
+ boolean allowAlternates, FoundMethodBuilder found,
+ MethodBuilderWithChunkTypes insert, ParameterSpec... params) {
this.name = name;
this.stateValueName = stateValueName;
this.requiresRowKeyChunk = requiresRowKeyChunk;
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/typed/TypedHasherFactory.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/typed/TypedHasherFactory.java
index ae4663a6af4..601ae2fb3bc 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/by/typed/TypedHasherFactory.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/by/typed/TypedHasherFactory.java
@@ -4,31 +4,36 @@
package io.deephaven.engine.table.impl.by.typed;
import com.squareup.javapoet.*;
-
import io.deephaven.UncheckedDeephavenException;
import io.deephaven.base.verify.Assert;
import io.deephaven.chunk.*;
import io.deephaven.chunk.attributes.Values;
import io.deephaven.chunk.util.hashing.CharChunkHasher;
-import io.deephaven.engine.context.ExecutionContext;
import io.deephaven.configuration.Configuration;
-import io.deephaven.engine.rowset.*;
+import io.deephaven.engine.context.ExecutionContext;
+import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.RowSet;
+import io.deephaven.engine.rowset.RowSetBuilderRandom;
import io.deephaven.engine.rowset.chunkattributes.OrderedRowKeys;
import io.deephaven.engine.rowset.chunkattributes.RowKeys;
import io.deephaven.engine.table.ColumnSource;
+import io.deephaven.engine.table.impl.MultiJoinModifiedSlotTracker;
import io.deephaven.engine.table.impl.NaturalJoinModifiedSlotTracker;
import io.deephaven.engine.table.impl.asofjoin.RightIncrementalAsOfJoinStateManagerTypedBase;
import io.deephaven.engine.table.impl.asofjoin.StaticAsOfJoinStateManagerTypedBase;
import io.deephaven.engine.table.impl.asofjoin.TypedAsOfJoinFactory;
-import io.deephaven.engine.table.impl.naturaljoin.RightIncrementalNaturalJoinStateManagerTypedBase;
import io.deephaven.engine.table.impl.by.*;
+import io.deephaven.engine.table.impl.multijoin.IncrementalMultiJoinStateManagerTypedBase;
+import io.deephaven.engine.table.impl.multijoin.StaticMultiJoinStateManagerTypedBase;
+import io.deephaven.engine.table.impl.multijoin.TypedMultiJoinFactory;
import io.deephaven.engine.table.impl.naturaljoin.IncrementalNaturalJoinStateManagerTypedBase;
+import io.deephaven.engine.table.impl.naturaljoin.RightIncrementalNaturalJoinStateManagerTypedBase;
import io.deephaven.engine.table.impl.naturaljoin.StaticNaturalJoinStateManagerTypedBase;
import io.deephaven.engine.table.impl.naturaljoin.TypedNaturalJoinFactory;
import io.deephaven.engine.table.impl.sources.*;
import io.deephaven.engine.table.impl.sources.immutable.*;
-import io.deephaven.engine.table.impl.updateby.hashing.UpdateByStateManagerTypedBase;
import io.deephaven.engine.table.impl.updateby.hashing.TypedUpdateByFactory;
+import io.deephaven.engine.table.impl.updateby.hashing.UpdateByStateManagerTypedBase;
import io.deephaven.util.QueryConstants;
import io.deephaven.util.compare.CharComparisons;
import org.apache.commons.lang3.mutable.MutableInt;
@@ -333,6 +338,79 @@ public static HasherConfig hasherConfigForBase(Class baseClass) {
builder.addProbe(new HasherConfig.ProbeSpec("probeHashTable", "rowState",
true, TypedUpdateByFactory::incrementalProbeFound, TypedUpdateByFactory::incrementalProbeMissing,
outputPositions));
+ } else if (baseClass.equals(StaticMultiJoinStateManagerTypedBase.class)) {
+ builder.classPrefix("StaticMultiJoinHasher").packageGroup("multijoin").packageMiddle("staticopen")
+ .openAddressedAlternate(false)
+ .stateType(int.class).mainStateName("slotToOutputRow")
+ .emptyStateName("EMPTY_OUTPUT_ROW")
+ .includeOriginalSources(true)
+ .supportRehash(true)
+ .moveMainFull(TypedMultiJoinFactory::staticMoveMainFull)
+ .alwaysMoveMain(true)
+ .rehashFullSetup(TypedMultiJoinFactory::staticRehashSetup);
+
+
+ builder.addBuild(new HasherConfig.BuildSpec("buildFromTable", "slotValue",
+ true, true, TypedMultiJoinFactory::staticBuildLeftFound,
+ TypedMultiJoinFactory::staticBuildLeftInsert,
+ ParameterSpec.builder(TypeName.get(LongArraySource.class), "tableRedirSource").build(),
+ ParameterSpec.builder(long.class, "tableNumber").build()));
+ } else if (baseClass.equals(IncrementalMultiJoinStateManagerTypedBase.class)) {
+ final ParameterSpec modifiedSlotTrackerParam =
+ ParameterSpec.builder(MultiJoinModifiedSlotTracker.class, "modifiedSlotTracker").build();
+ final ParameterSpec tableRedirSourceParam =
+ ParameterSpec.builder(TypeName.get(LongArraySource.class), "tableRedirSource").build();
+ final ParameterSpec tableNumberParam =
+ ParameterSpec.builder(int.class, "tableNumber").build();
+ final ParameterSpec flagParam =
+ ParameterSpec.builder(byte.class, "trackerFlag").build();
+
+ builder.classPrefix("IncrementalMultiJoinHasher").packageGroup("multijoin")
+ .packageMiddle("incopen")
+ .openAddressedAlternate(true)
+ .stateType(int.class).mainStateName("slotToOutputRow")
+ .overflowOrAlternateStateName("alternateSlotToOutputRow")
+ .emptyStateName("EMPTY_OUTPUT_ROW")
+ .includeOriginalSources(true)
+ .supportRehash(true)
+ .moveMainFull(TypedMultiJoinFactory::incrementalMoveMainFull)
+ .moveMainAlternate(TypedMultiJoinFactory::incrementalMoveMainAlternate)
+ .alwaysMoveMain(true)
+ .rehashFullSetup(TypedMultiJoinFactory::incrementalRehashSetup);
+
+ builder.addBuild(new HasherConfig.BuildSpec("buildFromTable", "slotValue",
+ true, true,
+ TypedMultiJoinFactory::incrementalBuildLeftFound,
+ TypedMultiJoinFactory::incrementalBuildLeftInsert,
+ tableRedirSourceParam,
+ tableNumberParam,
+ modifiedSlotTrackerParam,
+ flagParam));
+
+ builder.addProbe(new HasherConfig.ProbeSpec("remove", "slotValue", true,
+ TypedMultiJoinFactory::incrementalRemoveLeftFound,
+ TypedMultiJoinFactory::incrementalRemoveLeftMissing,
+ tableRedirSourceParam,
+ tableNumberParam,
+ modifiedSlotTrackerParam,
+ flagParam));
+
+ builder.addProbe(new HasherConfig.ProbeSpec("shift", "slotValue", true,
+ TypedMultiJoinFactory::incrementalShiftLeftFound,
+ TypedMultiJoinFactory::incrementalShiftLeftMissing,
+ tableRedirSourceParam,
+ tableNumberParam,
+ modifiedSlotTrackerParam,
+ flagParam,
+ ParameterSpec.builder(long.class, "shiftDelta").build()));
+
+ builder.addProbe(new HasherConfig.ProbeSpec("modify", "slotValue", false,
+ TypedMultiJoinFactory::incrementalModifyLeftFound,
+ TypedMultiJoinFactory::incrementalModifyLeftMissing,
+ tableRedirSourceParam,
+ tableNumberParam,
+ modifiedSlotTrackerParam,
+ flagParam));
} else {
throw new UnsupportedOperationException("Unknown class to make: " + baseClass);
}
@@ -455,6 +533,26 @@ public static T make(HasherConfig hasherConfig, ColumnSource>[] tableKe
if (pregeneratedHasher != null) {
return pregeneratedHasher;
}
+ } else if (hasherConfig.baseClass
+ .equals(StaticMultiJoinStateManagerTypedBase.class)) {
+ // noinspection unchecked
+ T pregeneratedHasher =
+ (T) io.deephaven.engine.table.impl.multijoin.typed.staticopen.gen.TypedHashDispatcher
+ .dispatch(tableKeySources, originalKeySources, tableSize, maximumLoadFactor,
+ targetLoadFactor);
+ if (pregeneratedHasher != null) {
+ return pregeneratedHasher;
+ }
+ } else if (hasherConfig.baseClass
+ .equals(IncrementalMultiJoinStateManagerTypedBase.class)) {
+ // noinspection unchecked
+ T pregeneratedHasher =
+ (T) io.deephaven.engine.table.impl.multijoin.typed.incopen.gen.TypedHashDispatcher
+ .dispatch(tableKeySources, originalKeySources, tableSize, maximumLoadFactor,
+ targetLoadFactor);
+ if (pregeneratedHasher != null) {
+ return pregeneratedHasher;
+ }
}
}
@@ -1120,7 +1218,7 @@ private static void doBuildSearch(HasherConfig> hasherConfig, HasherConfig.Bui
for (int ii = 0; ii < chunkTypes.length; ++ii) {
builder.addStatement("mainKeySource$L.set($L, k$L)", ii, tableLocationName, ii);
}
- buildSpec.insert.accept(hasherConfig, builder);
+ buildSpec.insert.accept(hasherConfig, chunkTypes, builder);
}
builder.addStatement("break");
builder.nextControlFlow("else if ("
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/IncrementalMultiJoinStateManagerTypedBase.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/IncrementalMultiJoinStateManagerTypedBase.java
new file mode 100644
index 00000000000..17fb4d2015d
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/IncrementalMultiJoinStateManagerTypedBase.java
@@ -0,0 +1,542 @@
+/**
+ * Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.engine.table.impl.multijoin;
+
+import io.deephaven.base.verify.Assert;
+import io.deephaven.base.verify.Require;
+import io.deephaven.chunk.Chunk;
+import io.deephaven.chunk.ChunkType;
+import io.deephaven.chunk.attributes.Values;
+import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.RowSet;
+import io.deephaven.engine.rowset.RowSetShiftData;
+import io.deephaven.engine.rowset.WritableRowSet;
+import io.deephaven.engine.table.ColumnSource;
+import io.deephaven.engine.table.Table;
+import io.deephaven.engine.table.WritableColumnSource;
+import io.deephaven.engine.table.impl.MultiJoinModifiedSlotTracker;
+import io.deephaven.engine.table.impl.MultiJoinStateManager;
+import io.deephaven.engine.table.impl.sources.ArrayBackedColumnSource;
+import io.deephaven.engine.table.impl.sources.InMemoryColumnSource;
+import io.deephaven.engine.table.impl.sources.LongArraySource;
+import io.deephaven.engine.table.impl.sources.immutable.ImmutableIntArraySource;
+import io.deephaven.engine.table.impl.sources.immutable.ImmutableLongArraySource;
+import io.deephaven.engine.table.impl.util.*;
+import io.deephaven.util.QueryConstants;
+import org.apache.commons.lang3.mutable.MutableInt;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static io.deephaven.engine.rowset.RowSequence.NULL_ROW_KEY;
+import static io.deephaven.engine.table.impl.JoinControl.CHUNK_SIZE;
+import static io.deephaven.engine.table.impl.JoinControl.MAX_TABLE_SIZE;
+import static io.deephaven.engine.table.impl.MultiJoinModifiedSlotTracker.FLAG_SHIFT;
+import static io.deephaven.engine.table.impl.util.TypedHasherUtil.getKeyChunks;
+import static io.deephaven.engine.table.impl.util.TypedHasherUtil.getPrevKeyChunks;
+import static io.deephaven.util.QueryConstants.NULL_BYTE;
+
+public abstract class IncrementalMultiJoinStateManagerTypedBase implements MultiJoinStateManager {
+ protected final ColumnSource>[] keySourcesForErrorMessages;
+ private final List redirectionSources = new ArrayList<>();
+
+ public static final long NO_REDIRECTION = QueryConstants.NULL_LONG;
+ public static final int EMPTY_OUTPUT_ROW = QueryConstants.NULL_INT;
+ public static final long EMPTY_COOKIE_SLOT = -1;
+
+ /** The number of slots in our hash table. */
+ protected int tableSize;
+ /**
+ * The number of slots in our alternate table, to start with "1" is a lie, but rehashPointer is zero; so our
+ * location value is positive and can be compared against rehashPointer safely.
+ */
+ protected int alternateTableSize = 1;
+
+ /** The number of entries in our hash table in use. */
+ protected int numEntries = 0;
+
+ /**
+ * The table will be rehashed to a load factor of targetLoadFactor if our loadFactor exceeds maximumLoadFactor.
+ */
+ private final double maximumLoadFactor;
+
+ /** The keys for our hash entries. */
+ protected final ChunkType[] chunkTypes;
+ protected final WritableColumnSource>[] mainKeySources;
+ protected final WritableColumnSource>[] alternateKeySources;
+
+ /** The output sources representing the keys of our joined table. */
+ protected final WritableColumnSource[] outputKeySources;
+
+ /** Store sentinel information and maps hash slots to output row keys. */
+ protected ImmutableIntArraySource slotToOutputRow = new ImmutableIntArraySource();
+ protected ImmutableIntArraySource alternateSlotToOutputRow;
+
+ protected ImmutableLongArraySource mainModifiedTrackerCookieSource = new ImmutableLongArraySource();
+ protected ImmutableLongArraySource alternateModifiedTrackerCookieSource;
+
+ /** how much of the alternate sources are necessary to rehash? */
+ protected int rehashPointer = 0;
+
+ protected IncrementalMultiJoinStateManagerTypedBase(ColumnSource>[] tableKeySources,
+ ColumnSource>[] keySourcesForErrorMessages, int tableSize, double maximumLoadFactor) {
+ this.keySourcesForErrorMessages = keySourcesForErrorMessages;
+
+ // we start out with a chunk sized table, and will grow by rehashing as states are added
+ this.tableSize = tableSize;
+ Require.leq(tableSize, "tableSize", MAX_TABLE_SIZE);
+ Require.gtZero(tableSize, "tableSize");
+ Require.eq(Integer.bitCount(tableSize), "Integer.bitCount(tableSize)", 1);
+ Require.inRange(maximumLoadFactor, 0.0, 0.95, "maximumLoadFactor");
+
+ mainKeySources = new WritableColumnSource[tableKeySources.length];
+ alternateKeySources = new WritableColumnSource[tableKeySources.length];
+ chunkTypes = new ChunkType[tableKeySources.length];
+
+ outputKeySources = new WritableColumnSource[tableKeySources.length];
+
+ for (int ii = 0; ii < tableKeySources.length; ++ii) {
+ chunkTypes[ii] = tableKeySources[ii].getChunkType();
+ mainKeySources[ii] = InMemoryColumnSource.getImmutableMemoryColumnSource(tableSize,
+ tableKeySources[ii].getType(), tableKeySources[ii].getComponentType());
+ outputKeySources[ii] = ArrayBackedColumnSource.getMemoryColumnSource(tableSize,
+ tableKeySources[ii].getType(), tableKeySources[ii].getComponentType());
+ }
+
+ this.maximumLoadFactor = maximumLoadFactor;
+
+ // This is called only once.
+ ensureCapacity(tableSize);
+ }
+
+ public int getTableCount() {
+ return redirectionSources.size();
+ }
+
+ private void ensureCapacity(int tableSize) {
+ slotToOutputRow.ensureCapacity(tableSize);
+ mainModifiedTrackerCookieSource.ensureCapacity(tableSize);
+ for (WritableColumnSource> mainKeySource : mainKeySources) {
+ mainKeySource.ensureCapacity(tableSize);
+ }
+ }
+
+ public static class BuildContext extends TypedHasherUtil.BuildOrProbeContext {
+ private BuildContext(ColumnSource>[] buildSources, int chunkSize) {
+ super(buildSources, chunkSize);
+ }
+
+ final MutableInt rehashCredits = new MutableInt(0);
+ }
+
+ public static class ProbeContext extends TypedHasherUtil.BuildOrProbeContext {
+ private ProbeContext(ColumnSource>[] probeSources, int chunkSize) {
+ super(probeSources, chunkSize);
+ }
+ }
+
+ public BuildContext makeBuildContext(ColumnSource>[] buildSources, long maxSize) {
+ return new BuildContext(buildSources, (int) Math.min(CHUNK_SIZE, maxSize));
+ }
+
+ public ProbeContext makeProbeContext(ColumnSource>[] probeSources, long maxSize) {
+ return new ProbeContext(probeSources, (int) Math.min(CHUNK_SIZE, maxSize));
+ }
+
+ private class BuildHandler implements TypedHasherUtil.BuildHandler {
+ final LongArraySource tableRedirSource;
+ final int tableNumber;
+ final MultiJoinModifiedSlotTracker modifiedSlotTracker;
+ final byte trackerFlag;
+
+ private BuildHandler(LongArraySource tableRedirSource, int tableNumber) {
+ this.tableRedirSource = tableRedirSource;
+ this.tableNumber = tableNumber;
+ this.modifiedSlotTracker = null;
+ this.trackerFlag = NULL_BYTE;
+ }
+
+ private BuildHandler(LongArraySource tableRedirSource, int tableNumber,
+ @NotNull MultiJoinModifiedSlotTracker slotTracker, byte trackerFlag) {
+ this.tableRedirSource = tableRedirSource;
+ this.tableNumber = tableNumber;
+ this.modifiedSlotTracker = slotTracker;
+ this.trackerFlag = trackerFlag;
+ }
+
+ @Override
+ public void doBuild(RowSequence rows, Chunk[] sourceKeyChunks) {
+ final long maxSize = numEntries + rows.intSize();
+ tableRedirSource.ensureCapacity(maxSize);
+ for (WritableColumnSource> src : outputKeySources) {
+ src.ensureCapacity(maxSize);
+ }
+ buildFromTable(rows, sourceKeyChunks, tableRedirSource, tableNumber, modifiedSlotTracker,
+ trackerFlag);
+ }
+ }
+
+ private static abstract class ProbeHandler implements TypedHasherUtil.ProbeHandler {
+ final LongArraySource tableRedirSource;
+ final int tableNumber;
+ final MultiJoinModifiedSlotTracker modifiedSlotTracker;
+ final byte trackerFlag;
+
+ private ProbeHandler(LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ this.tableRedirSource = tableRedirSource;
+ this.tableNumber = tableNumber;
+ this.modifiedSlotTracker = modifiedSlotTracker;
+ this.trackerFlag = trackerFlag;
+ }
+ }
+
+ private class RemoveHandler extends ProbeHandler {
+ private RemoveHandler(LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ super(tableRedirSource, tableNumber, modifiedSlotTracker, trackerFlag);
+ }
+
+ @Override
+ public void doProbe(RowSequence rows, Chunk[] sourceKeyChunks) {
+ remove(rows, sourceKeyChunks, tableRedirSource, tableNumber, modifiedSlotTracker, trackerFlag);
+ }
+ }
+
+ private class ShiftHandler extends ProbeHandler {
+ final long shiftDelta;
+
+ private ShiftHandler(LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag, long shiftDelta) {
+ super(tableRedirSource, tableNumber, modifiedSlotTracker, trackerFlag);
+ this.shiftDelta = shiftDelta;
+ }
+
+ @Override
+ public void doProbe(RowSequence rows, Chunk[] sourceKeyChunks) {
+ shift(rows, sourceKeyChunks, tableRedirSource, tableNumber, modifiedSlotTracker, trackerFlag,
+ shiftDelta);
+ }
+ }
+
+ private class ModifyHandler extends ProbeHandler {
+ private ModifyHandler(LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ super(tableRedirSource, tableNumber, modifiedSlotTracker, trackerFlag);
+ }
+
+ @Override
+ public void doProbe(RowSequence rows, Chunk[] sourceKeyChunks) {
+ modify(rows, sourceKeyChunks, tableRedirSource, tableNumber, modifiedSlotTracker, trackerFlag);
+ }
+ }
+
+ @Override
+ public void build(final Table table, ColumnSource>[] keySources, int tableNumber) {
+ if (table.isEmpty()) {
+ return;
+ }
+ final LongArraySource tableRedirSource = redirectionSources.get(tableNumber);
+ try (final BuildContext bc = makeBuildContext(keySources, table.size())) {
+ buildTable(true, bc, table.getRowSet(), keySources, new BuildHandler(tableRedirSource, tableNumber));
+ }
+ }
+
+ public void processRemoved(final RowSet rowSet, ColumnSource>[] sources, int tableNumber,
+ @NotNull MultiJoinModifiedSlotTracker slotTracker, byte trackerFlag) {
+ if (rowSet.isEmpty()) {
+ return;
+ }
+
+ Assert.geq(redirectionSources.size(), "redirectionSources.size()", tableNumber, "tableNumber");
+ final LongArraySource tableRedirSource = redirectionSources.get(tableNumber);
+
+ try (final ProbeContext pc = makeProbeContext(sources, rowSet.size())) {
+ probeTable(pc, rowSet, true, sources,
+ new RemoveHandler(tableRedirSource, tableNumber, slotTracker, trackerFlag));
+ }
+ }
+
+ public void processShifts(final RowSet rowSet, final RowSetShiftData rowSetShiftData, ColumnSource>[] sources,
+ int tableNumber, @NotNull MultiJoinModifiedSlotTracker slotTracker) {
+ if (rowSet.isEmpty() || rowSetShiftData.empty()) {
+ return;
+ }
+
+ Assert.geq(redirectionSources.size(), "redirectionSources.size()", tableNumber, "tableNumber");
+ final LongArraySource tableRedirSource = redirectionSources.get(tableNumber);
+
+ // Re-use the probe context for each shift range.
+ try (final ProbeContext pc = makeProbeContext(sources, rowSet.size())) {
+ final RowSetShiftData.Iterator sit = rowSetShiftData.applyIterator();
+ while (sit.hasNext()) {
+ sit.next();
+ try (final WritableRowSet rowSetToShift =
+ rowSet.subSetByKeyRange(sit.beginRange(), sit.endRange())) {
+ probeTable(pc, rowSetToShift, true, sources, new ShiftHandler(tableRedirSource, tableNumber,
+ slotTracker, FLAG_SHIFT, sit.shiftDelta()));
+ }
+ }
+ }
+ }
+
+ public void processModified(final RowSet rowSet, ColumnSource>[] sources, int tableNumber,
+ @NotNull MultiJoinModifiedSlotTracker slotTracker, byte trackerFlag) {
+ if (rowSet.isEmpty()) {
+ return;
+ }
+
+ Assert.geq(redirectionSources.size(), "redirectionSources.size()", tableNumber, "tableNumber");
+ final LongArraySource tableRedirSource = redirectionSources.get(tableNumber);
+
+ try (final ProbeContext pc = makeProbeContext(sources, rowSet.size())) {
+ probeTable(pc, rowSet, false, sources,
+ new ModifyHandler(tableRedirSource, tableNumber, slotTracker, trackerFlag));
+ }
+ }
+
+ public void processAdded(final RowSet rowSet, ColumnSource>[] sources, int tableNumber,
+ @NotNull MultiJoinModifiedSlotTracker slotTracker, byte trackerFlag) {
+ if (rowSet.isEmpty()) {
+ return;
+ }
+
+ Assert.geq(redirectionSources.size(), "redirectionSources.size()", tableNumber, "tableNumber");
+ final LongArraySource tableRedirSource = redirectionSources.get(tableNumber);
+
+ try (final BuildContext bc = makeBuildContext(sources, rowSet.size())) {
+ buildTable(false, bc, rowSet, sources,
+ new BuildHandler(tableRedirSource, tableNumber, slotTracker, trackerFlag));
+ }
+ }
+
+ protected abstract void buildFromTable(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag);
+
+ protected abstract void remove(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag);
+
+ protected abstract void shift(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag, long shiftDelta);
+
+ protected abstract void modify(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag);
+
+ abstract protected void migrateFront();
+
+ private void buildTable(
+ final boolean initialBuild,
+ final BuildContext bc,
+ final RowSequence buildRows,
+ final ColumnSource>[] buildSources,
+ final BuildHandler buildHandler) {
+ try (final RowSequence.Iterator rsIt = buildRows.getRowSequenceIterator()) {
+ // noinspection unchecked
+ final Chunk[] sourceKeyChunks = new Chunk[buildSources.length];
+
+ while (rsIt.hasMore()) {
+ final RowSequence rows = rsIt.getNextRowSequenceWithLength(bc.chunkSize);
+ final int nextChunkSize = rows.intSize();
+ while (doRehash(initialBuild, bc.rehashCredits, nextChunkSize)) {
+ migrateFront();
+ }
+
+ getKeyChunks(buildSources, bc.getContexts, sourceKeyChunks, rows);
+
+ final long oldEntries = numEntries;
+ buildHandler.doBuild(rows, sourceKeyChunks);
+ final long entriesAdded = numEntries - oldEntries;
+ // if we actually added anything, then take away from the "equity" we've built up rehashing, otherwise
+ // don't penalize this build call with additional rehashing
+ bc.rehashCredits.subtract(entriesAdded);
+
+ bc.resetSharedContexts();
+ }
+ }
+ }
+
+ private void probeTable(
+ final ProbeContext pc,
+ final RowSequence probeRows,
+ final boolean usePrev,
+ final ColumnSource>[] probeSources,
+ final TypedHasherUtil.ProbeHandler handler) {
+ try (final RowSequence.Iterator rsIt = probeRows.getRowSequenceIterator()) {
+ // noinspection unchecked
+ final Chunk[] sourceKeyChunks = new Chunk[probeSources.length];
+
+ while (rsIt.hasMore()) {
+ final RowSequence rows = rsIt.getNextRowSequenceWithLength(pc.chunkSize);
+
+ if (usePrev) {
+ getPrevKeyChunks(probeSources, pc.getContexts, sourceKeyChunks, rows);
+ } else {
+ getKeyChunks(probeSources, pc.getContexts, sourceKeyChunks, rows);
+ }
+
+ handler.doProbe(rows, sourceKeyChunks);
+
+ pc.resetSharedContexts();
+ }
+ }
+ }
+
+ /**
+ * @param fullRehash should we rehash the entire table (if false, we rehash incrementally)
+ * @param rehashCredits the number of entries this operation has rehashed (input/output)
+ * @param nextChunkSize the size of the chunk we are processing
+ * @return true if a front migration is required
+ */
+ public boolean doRehash(boolean fullRehash, MutableInt rehashCredits, int nextChunkSize) {
+ if (rehashPointer > 0) {
+ final int requiredRehash = nextChunkSize - rehashCredits.intValue();
+ if (requiredRehash <= 0) {
+ return false;
+ }
+
+ // before building, we need to do at least as much rehash work as we would do build work
+ rehashCredits.add(rehashInternalPartial(requiredRehash));
+ if (rehashPointer == 0) {
+ clearAlternate();
+ }
+ }
+
+ int oldTableSize = tableSize;
+ while (rehashRequired(nextChunkSize)) {
+ tableSize *= 2;
+
+ if (tableSize < 0 || tableSize > MAX_TABLE_SIZE) {
+ throw new UnsupportedOperationException("Hash table exceeds maximum size!");
+ }
+ }
+
+ if (oldTableSize == tableSize) {
+ return false;
+ }
+
+ // we can't give the caller credit for rehashes with the old table, we need to begin migrating things again
+ if (rehashCredits.intValue() > 0) {
+ rehashCredits.setValue(0);
+ }
+
+ if (fullRehash) {
+ // if we are doing a full rehash, we need to ditch the alternate
+ if (rehashPointer > 0) {
+ rehashInternalPartial(numEntries);
+ clearAlternate();
+ }
+
+ rehashInternalFull(oldTableSize);
+
+ return false;
+ }
+
+ Assert.eqZero(rehashPointer, "rehashPointer");
+
+ if (numEntries == 0) {
+ return false;
+ }
+ newAlternate();
+ alternateTableSize = oldTableSize;
+ rehashPointer = alternateTableSize;
+ return true;
+ }
+
+ protected void newAlternate() {
+ alternateSlotToOutputRow = slotToOutputRow;
+ slotToOutputRow = new ImmutableIntArraySource();
+ slotToOutputRow.ensureCapacity(tableSize);
+
+ alternateModifiedTrackerCookieSource = mainModifiedTrackerCookieSource;
+ mainModifiedTrackerCookieSource = new ImmutableLongArraySource();
+ mainModifiedTrackerCookieSource.ensureCapacity(tableSize);
+
+ for (int ii = 0; ii < mainKeySources.length; ++ii) {
+ alternateKeySources[ii] = mainKeySources[ii];
+ mainKeySources[ii] = InMemoryColumnSource.getImmutableMemoryColumnSource(tableSize,
+ alternateKeySources[ii].getType(), alternateKeySources[ii].getComponentType());
+ mainKeySources[ii].ensureCapacity(tableSize);
+ }
+ }
+
+ protected void clearAlternate() {
+ alternateSlotToOutputRow = null;
+ alternateModifiedTrackerCookieSource = null;
+ for (int ii = 0; ii < mainKeySources.length; ++ii) {
+ alternateKeySources[ii] = null;
+ }
+ alternateTableSize = 1;
+ }
+
+ public boolean rehashRequired(int nextChunkSize) {
+ return (numEntries + nextChunkSize) > (tableSize * maximumLoadFactor);
+ }
+
+ abstract protected void rehashInternalFull(final int oldSize);
+
+ /**
+ * @param numEntriesToRehash number of entries to rehash into main table
+ * @return actual number of entries rehashed
+ */
+ protected abstract int rehashInternalPartial(int numEntriesToRehash);
+
+ protected int hashToTableLocation(int hash) {
+ return hash & (tableSize - 1);
+ }
+
+ protected int hashToTableLocationAlternate(int hash) {
+ return hash & (alternateTableSize - 1);
+ }
+
+ /** produce a pretty key for error messages. */
+ protected String keyString(Chunk[] sourceKeyChunks, int chunkPosition) {
+ return ChunkUtils.extractKeyStringFromChunks(chunkTypes, sourceKeyChunks, chunkPosition);
+ }
+
+ public void getCurrentRedirections(long slot, long[] redirections) {
+ for (int tt = 0; tt < redirectionSources.size(); ++tt) {
+ final long redirection = redirectionSources.get(tt).getLong(slot);
+ redirections[tt] = redirection == QueryConstants.NULL_LONG ? NULL_ROW_KEY : redirection;
+ }
+ }
+
+ public void startTrackingPrevRedirectionValues(int tableNumber) {
+ redirectionSources.get(tableNumber).startTrackingPrevValues();
+ }
+
+ @Override
+ public long getResultSize() {
+ return numEntries;
+ }
+
+ @Override
+ public ColumnSource>[] getKeyHashTableSources() {
+ return outputKeySources;
+ }
+
+ @Override
+ public RowRedirection getRowRedirectionForTable(int tableNumber) {
+ return new LongColumnSourceRowRedirection<>(redirectionSources.get(tableNumber));
+ }
+
+ @Override
+ public void ensureTableCapacity(int tables) {
+ while (redirectionSources.size() < tables) {
+ final LongArraySource newRedirection = new LongArraySource();
+ newRedirection.ensureCapacity(numEntries);
+ redirectionSources.add(newRedirection);
+ }
+ }
+
+ @Override
+ public void setTargetLoadFactor(double targetLoadFactor) {}
+
+ @Override
+ public void setMaximumLoadFactor(double maximumLoadFactor) {}
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/StaticMultiJoinStateManagerTypedBase.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/StaticMultiJoinStateManagerTypedBase.java
new file mode 100644
index 00000000000..c662260f42a
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/StaticMultiJoinStateManagerTypedBase.java
@@ -0,0 +1,214 @@
+/**
+ * Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.engine.table.impl.multijoin;
+
+import io.deephaven.base.verify.Require;
+import io.deephaven.chunk.Chunk;
+import io.deephaven.chunk.ChunkType;
+import io.deephaven.chunk.attributes.Values;
+import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.table.ColumnSource;
+import io.deephaven.engine.table.Table;
+import io.deephaven.engine.table.WritableColumnSource;
+import io.deephaven.engine.table.impl.MultiJoinStateManager;
+import io.deephaven.engine.table.impl.sources.ArrayBackedColumnSource;
+import io.deephaven.engine.table.impl.sources.InMemoryColumnSource;
+import io.deephaven.engine.table.impl.sources.LongArraySource;
+import io.deephaven.engine.table.impl.sources.immutable.ImmutableIntArraySource;
+import io.deephaven.engine.table.impl.util.*;
+import io.deephaven.engine.table.impl.util.TypedHasherUtil.BuildOrProbeContext.BuildContext;
+import io.deephaven.util.QueryConstants;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static io.deephaven.engine.table.impl.JoinControl.CHUNK_SIZE;
+import static io.deephaven.engine.table.impl.JoinControl.MAX_TABLE_SIZE;
+import static io.deephaven.engine.table.impl.util.TypedHasherUtil.getKeyChunks;
+
+public abstract class StaticMultiJoinStateManagerTypedBase implements MultiJoinStateManager {
+ protected final ColumnSource>[] keySourcesForErrorMessages;
+ private final List redirectionSources = new ArrayList<>();
+
+ public static final long NO_REDIRECTION = QueryConstants.NULL_LONG;
+ public static final int EMPTY_OUTPUT_ROW = QueryConstants.NULL_INT;
+
+ /** The number of slots in our hash table. */
+ protected int tableSize;
+
+ /** The number of entries in our hash table in use. */
+ protected int numEntries = 0;
+
+ /**
+ * The table will be rehashed to a load factor of targetLoadFactor if our loadFactor exceeds maximumLoadFactor.
+ */
+ private final double maximumLoadFactor;
+
+ /** The keys for our hash entries. */
+ protected final ChunkType[] chunkTypes;
+ protected final WritableColumnSource>[] mainKeySources;
+
+ /** The output sources representing the keys of our joined table. */
+ protected final WritableColumnSource[] outputKeySources;
+
+ /** Store sentinel information and maps hash slots to output row keys. */
+ protected ImmutableIntArraySource slotToOutputRow = new ImmutableIntArraySource();
+
+ protected StaticMultiJoinStateManagerTypedBase(ColumnSource>[] tableKeySources,
+ ColumnSource>[] keySourcesForErrorMessages,
+ int tableSize,
+ double maximumLoadFactor) {
+ this.keySourcesForErrorMessages = keySourcesForErrorMessages;
+
+ this.tableSize = tableSize;
+ Require.leq(tableSize, "tableSize", MAX_TABLE_SIZE);
+ Require.gtZero(tableSize, "tableSize");
+ Require.eq(Integer.bitCount(tableSize), "Integer.bitCount(tableSize)", 1);
+ Require.inRange(maximumLoadFactor, 0.0, 0.95, "maximumLoadFactor");
+
+ mainKeySources = new WritableColumnSource>[tableKeySources.length];
+ chunkTypes = new ChunkType[tableKeySources.length];
+
+ outputKeySources = new WritableColumnSource>[tableKeySources.length];
+
+ for (int ii = 0; ii < tableKeySources.length; ++ii) {
+ chunkTypes[ii] = tableKeySources[ii].getChunkType();
+ mainKeySources[ii] = InMemoryColumnSource.getImmutableMemoryColumnSource(tableSize,
+ tableKeySources[ii].getType(), tableKeySources[ii].getComponentType());
+ outputKeySources[ii] = ArrayBackedColumnSource.getMemoryColumnSource(tableSize,
+ tableKeySources[ii].getType(), tableKeySources[ii].getComponentType());
+ }
+
+ this.maximumLoadFactor = maximumLoadFactor;
+
+ // Called only once, here in the constructor.
+ ensureCapacity(tableSize);
+ }
+
+ private void ensureCapacity(int tableSize) {
+ slotToOutputRow.ensureCapacity(tableSize);
+ for (WritableColumnSource> mainKeySource : mainKeySources) {
+ mainKeySource.ensureCapacity(tableSize);
+ }
+ }
+
+ BuildContext makeBuildContext(ColumnSource>[] buildSources, long maxSize) {
+ return new BuildContext(buildSources, (int) Math.min(CHUNK_SIZE, maxSize));
+ }
+
+ @Override
+ public void build(final Table table, ColumnSource>[] keySources, int tableNumber) {
+ if (table.isEmpty()) {
+ return;
+ }
+ final LongArraySource tableRedirSource = redirectionSources.get(tableNumber);
+ try (final BuildContext bc = makeBuildContext(keySources, table.size())) {
+ buildTable(bc, table.getRowSet(), keySources, new BuildHandler(tableRedirSource, tableNumber));
+ }
+ }
+
+ private class BuildHandler implements TypedHasherUtil.BuildHandler {
+ final LongArraySource tableRedirSource;
+ final long tableNumber;
+
+ private BuildHandler(LongArraySource tableRedirSource, long tableNumber) {
+ this.tableRedirSource = tableRedirSource;
+ this.tableNumber = tableNumber;
+ }
+
+ @Override
+ public void doBuild(RowSequence rows, Chunk[] sourceKeyChunks) {
+ final long maxSize = numEntries + rows.intSize();
+ tableRedirSource.ensureCapacity(maxSize);
+ for (WritableColumnSource> src : outputKeySources) {
+ src.ensureCapacity(maxSize);
+ }
+ buildFromTable(rows, sourceKeyChunks, tableRedirSource, tableNumber);
+ }
+ }
+
+ protected abstract void buildFromTable(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, long tableNumber);
+
+ protected void buildTable(
+ final BuildContext bc,
+ final RowSequence buildRows,
+ final ColumnSource>[] buildSources,
+ final TypedHasherUtil.BuildHandler buildHandler) {
+ try (final RowSequence.Iterator rsIt = buildRows.getRowSequenceIterator()) {
+ // noinspection unchecked
+ final Chunk[] sourceKeyChunks = new Chunk[buildSources.length];
+
+ while (rsIt.hasMore()) {
+ final RowSequence rows = rsIt.getNextRowSequenceWithLength(bc.chunkSize);
+
+ doRehash(rows.intSize());
+
+ getKeyChunks(buildSources, bc.getContexts, sourceKeyChunks, rows);
+
+ buildHandler.doBuild(rows, sourceKeyChunks);
+
+ bc.resetSharedContexts();
+ }
+ }
+ }
+
+ public void doRehash(final int nextChunkSize) {
+ final int oldSize = tableSize;
+ while (rehashRequired(nextChunkSize)) {
+ tableSize *= 2;
+ if (tableSize < 0 || tableSize > MAX_TABLE_SIZE) {
+ throw new UnsupportedOperationException("Hash table exceeds maximum size!");
+ }
+ }
+ if (tableSize > oldSize) {
+ rehashInternalFull(oldSize);
+ }
+ }
+
+ public boolean rehashRequired(int nextChunkSize) {
+ return (numEntries + nextChunkSize) > (tableSize * maximumLoadFactor);
+ }
+
+ abstract protected void rehashInternalFull(final int oldSize);
+
+ protected int hashToTableLocation(int hash) {
+ return hash & (tableSize - 1);
+ }
+
+ /** produce a pretty key for error messages. */
+ protected String keyString(Chunk[] sourceKeyChunks, int chunkPosition) {
+ return ChunkUtils.extractKeyStringFromChunks(chunkTypes, sourceKeyChunks, chunkPosition);
+ }
+
+ @Override
+ public long getResultSize() {
+ return numEntries;
+ }
+
+ @Override
+ public ColumnSource>[] getKeyHashTableSources() {
+ return outputKeySources;
+ }
+
+ @Override
+ public RowRedirection getRowRedirectionForTable(int tableNumber) {
+ return new LongColumnSourceRowRedirection<>(redirectionSources.get(tableNumber));
+ }
+
+ @Override
+ public void ensureTableCapacity(int tables) {
+ while (redirectionSources.size() < tables) {
+ final LongArraySource newRedirection = new LongArraySource();
+ newRedirection.ensureCapacity(numEntries);
+ redirectionSources.add(newRedirection);
+ }
+ }
+
+ @Override
+ public void setTargetLoadFactor(double targetLoadFactor) {}
+
+ @Override
+ public void setMaximumLoadFactor(double maximumLoadFactor) {}
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/TypedMultiJoinFactory.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/TypedMultiJoinFactory.java
new file mode 100644
index 00000000000..e4dda8648af
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/TypedMultiJoinFactory.java
@@ -0,0 +1,151 @@
+/**
+ * Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
+ */
+package io.deephaven.engine.table.impl.multijoin;
+
+import com.squareup.javapoet.CodeBlock;
+import io.deephaven.chunk.ChunkType;
+import io.deephaven.engine.table.impl.by.typed.HasherConfig;
+
+public class TypedMultiJoinFactory {
+ public static void staticBuildLeftInsert(HasherConfig> hasherConfig, ChunkType[] chunkTypes,
+ CodeBlock.Builder builder) {
+ builder.addStatement("final int outputKey = numEntries - 1");
+ builder.addStatement("slotToOutputRow.set(tableLocation, outputKey)");
+ builder.addStatement("tableRedirSource.set(outputKey, rowKeyChunk.get(chunkPosition))");
+ for (int ii = 0; ii < chunkTypes.length; ii++) {
+ builder.addStatement("outputKeySources[" + ii + "].set((long)outputKey, k" + ii + ")");
+ }
+ }
+
+ public static void staticBuildLeftFound(HasherConfig> hasherConfig, boolean alternate,
+ CodeBlock.Builder builder) {
+ builder.beginControlFlow("if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION)");
+ builder.addStatement(
+ "throw new IllegalStateException(\"Duplicate key found for \" + keyString(sourceKeyChunks, chunkPosition) + \" in table \" + tableNumber + \".\")");
+ builder.endControlFlow();
+ builder.addStatement("tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition))");
+ }
+
+ public static void staticRehashSetup(CodeBlock.Builder builder) {}
+
+ public static void staticMoveMainFull(CodeBlock.Builder builder) {}
+
+ public static void incrementalRehashSetup(CodeBlock.Builder builder) {
+ builder.addStatement("final long [] oldModifiedCookie = mainModifiedTrackerCookieSource.getArray()");
+ builder.addStatement("final long [] destModifiedCookie = new long[tableSize]");
+ builder.addStatement("mainModifiedTrackerCookieSource.setArray(destModifiedCookie)");
+ }
+
+ public static void incrementalMoveMainFull(CodeBlock.Builder builder) {
+ builder.addStatement("destModifiedCookie[destinationTableLocation] = oldModifiedCookie[sourceBucket]");
+ }
+
+ public static void incrementalMoveMainAlternate(CodeBlock.Builder builder) {
+ builder.addStatement("final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(locationToMigrate)");
+ builder.addStatement("mainModifiedTrackerCookieSource.set(destinationTableLocation, cookie)");
+ builder.addStatement("alternateModifiedTrackerCookieSource.set(locationToMigrate, EMPTY_COOKIE_SLOT)");
+ }
+
+ public static void incrementalBuildLeftFound(HasherConfig> hasherConfig, boolean alternate,
+ CodeBlock.Builder builder) {
+ builder.beginControlFlow("if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION)");
+ builder.addStatement(
+ "throw new IllegalStateException(\"Duplicate key found for \" + keyString(sourceKeyChunks, chunkPosition) + \" in table \" + tableNumber + \".\")");
+ builder.endControlFlow();
+ builder.addStatement("tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition))");
+
+ builder.beginControlFlow("if (modifiedSlotTracker != null)");
+ if (!alternate) {
+ builder.addStatement("final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation)");
+ builder.addStatement(
+ "mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag))");
+ } else {
+ builder.addStatement(
+ "final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation)");
+ builder.addStatement(
+ "alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag))");
+ }
+ builder.endControlFlow();
+ }
+
+ public static void incrementalBuildLeftInsert(HasherConfig> hasherConfig, ChunkType[] chunkTypes,
+ CodeBlock.Builder builder) {
+ builder.addStatement("final int outputKey = numEntries - 1");
+ builder.addStatement("slotToOutputRow.set(tableLocation, outputKey)");
+ builder.addStatement("tableRedirSource.set(outputKey, rowKeyChunk.get(chunkPosition))");
+ for (int ii = 0; ii < chunkTypes.length; ii++) {
+ builder.addStatement("outputKeySources[" + ii + "].set((long)outputKey, k" + ii + ")");
+ }
+ builder.add("// NOTE: if there are other tables adding this row this cycle, we will add these into the slot\n");
+ builder.add(
+ "// tracker. However, when the modified slots are processed we will identify the output row as new\n");
+ builder.add("// for this cycle and ignore the incomplete tracker data.\n");
+ builder.addStatement("mainModifiedTrackerCookieSource.set(tableLocation, EMPTY_COOKIE_SLOT)");
+ }
+
+ public static void incrementalModifyLeftFound(HasherConfig> hasherConfig, boolean alternate,
+ CodeBlock.Builder builder) {
+ if (!alternate) {
+ builder.addStatement("final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation)");
+ builder.addStatement(
+ "mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag))");
+ } else {
+ builder.addStatement(
+ "final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation)");
+ builder.addStatement(
+ "alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag))");
+ }
+ }
+
+ public static void incrementalModifyLeftMissing(CodeBlock.Builder builder) {
+ builder.addStatement(
+ "throw new IllegalStateException(\"Matching row not found for \" + keyString(sourceKeyChunks, chunkPosition) + \" in table \" + tableNumber + \".\")");
+ }
+
+ public static void incrementalRemoveLeftFound(HasherConfig> hasherConfig, boolean alternate,
+ CodeBlock.Builder builder) {
+ builder.addStatement("final long mappedRowKey = tableRedirSource.getUnsafe(slotValue)");
+ builder.addStatement("tableRedirSource.set(slotValue, NO_REDIRECTION)");
+ builder.addStatement(
+ "Assert.eq(rowKeyChunk.get(chunkPosition), \"rowKey\", mappedRowKey, \"mappedRowKey\")");
+ if (!alternate) {
+ builder.addStatement("final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation)");
+ builder.addStatement(
+ "mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag))");
+ } else {
+ builder.addStatement(
+ "final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation)");
+ builder.addStatement(
+ "alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag))");
+ }
+ }
+
+ public static void incrementalRemoveLeftMissing(CodeBlock.Builder builder) {
+ builder.addStatement(
+ "throw new IllegalStateException(\"Matching row not found for \" + keyString(sourceKeyChunks, chunkPosition) + \" in table \" + tableNumber + \".\")");
+ }
+
+ public static void incrementalShiftLeftFound(HasherConfig> hasherConfig, boolean alternate,
+ CodeBlock.Builder builder) {
+ builder.addStatement("final long mappedRowKey = tableRedirSource.getUnsafe(slotValue)");
+ builder.addStatement(
+ "Assert.eq(rowKeyChunk.get(chunkPosition), \"rowKey\", mappedRowKey, \"mappedRowKey\")");
+ builder.addStatement("tableRedirSource.set(slotValue, mappedRowKey + shiftDelta)");
+ if (!alternate) {
+ builder.addStatement("final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation)");
+ builder.addStatement(
+ "mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag))");
+ } else {
+ builder.addStatement(
+ "final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation)");
+ builder.addStatement(
+ "alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag))");
+ }
+ }
+
+ public static void incrementalShiftLeftMissing(CodeBlock.Builder builder) {
+ builder.addStatement(
+ "throw new IllegalStateException(\"Matching row not found for \" + keyString(sourceKeyChunks, chunkPosition) + \" in table \" + tableNumber + \".\")");
+ }
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherByte.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherByte.java
new file mode 100644
index 00000000000..d90e8e6ece5
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherByte.java
@@ -0,0 +1,346 @@
+// DO NOT EDIT THIS CLASS, AUTOMATICALLY GENERATED BY io.deephaven.engine.table.impl.by.typed.TypedHasherFactory
+// Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.engine.table.impl.multijoin.typed.incopen.gen;
+
+import static io.deephaven.util.compare.ByteComparisons.eq;
+
+import io.deephaven.base.verify.Assert;
+import io.deephaven.chunk.ByteChunk;
+import io.deephaven.chunk.Chunk;
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.attributes.Values;
+import io.deephaven.chunk.util.hashing.ByteChunkHasher;
+import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.OrderedRowKeys;
+import io.deephaven.engine.table.ColumnSource;
+import io.deephaven.engine.table.impl.MultiJoinModifiedSlotTracker;
+import io.deephaven.engine.table.impl.multijoin.IncrementalMultiJoinStateManagerTypedBase;
+import io.deephaven.engine.table.impl.sources.LongArraySource;
+import io.deephaven.engine.table.impl.sources.immutable.ImmutableByteArraySource;
+import java.lang.Override;
+import java.util.Arrays;
+
+final class IncrementalMultiJoinHasherByte extends IncrementalMultiJoinStateManagerTypedBase {
+ private ImmutableByteArraySource mainKeySource0;
+
+ private ImmutableByteArraySource alternateKeySource0;
+
+ public IncrementalMultiJoinHasherByte(ColumnSource[] tableKeySources,
+ ColumnSource[] originalTableKeySources, int tableSize, double maximumLoadFactor,
+ double targetLoadFactor) {
+ super(tableKeySources, originalTableKeySources, tableSize, maximumLoadFactor);
+ this.mainKeySource0 = (ImmutableByteArraySource) super.mainKeySources[0];
+ this.mainKeySource0.ensureCapacity(tableSize);
+ }
+
+ private int nextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (tableSize - 1);
+ }
+
+ private int alternateNextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (alternateTableSize - 1);
+ }
+
+ protected void buildFromTable(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final ByteChunk keyChunk0 = sourceKeyChunks[0].asByteChunk();
+ final int chunkSize = keyChunk0.size();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final byte k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ int tableLocation = firstTableLocation;
+ MAIN_SEARCH: while (true) {
+ int slotValue = slotToOutputRow.getUnsafe(tableLocation);
+ if (slotValue == EMPTY_OUTPUT_ROW) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ int alternateTableLocation = firstAlternateTableLocation;
+ while (alternateTableLocation < rehashPointer) {
+ slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation);
+ if (slotValue == EMPTY_OUTPUT_ROW) {
+ break;
+ } else if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION) {
+ throw new IllegalStateException("Duplicate key found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition));
+ if (modifiedSlotTracker != null) {
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag));
+ }
+ break MAIN_SEARCH;
+ } else {
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ numEntries++;
+ mainKeySource0.set(tableLocation, k0);
+ final int outputKey = numEntries - 1;
+ slotToOutputRow.set(tableLocation, outputKey);
+ tableRedirSource.set(outputKey, rowKeyChunk.get(chunkPosition));
+ outputKeySources[0].set((long)outputKey, k0);
+ // NOTE: if there are other tables adding this row this cycle, we will add these into the slot
+ // tracker. However, when the modified slots are processed we will identify the output row as new
+ // for this cycle and ignore the incomplete tracker data.
+ mainModifiedTrackerCookieSource.set(tableLocation, EMPTY_COOKIE_SLOT);
+ break;
+ } else if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION) {
+ throw new IllegalStateException("Duplicate key found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition));
+ if (modifiedSlotTracker != null) {
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag));
+ }
+ break;
+ } else {
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ }
+ }
+ }
+
+ protected void remove(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final ByteChunk keyChunk0 = sourceKeyChunks[0].asByteChunk();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final byte k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ tableRedirSource.set(slotValue, NO_REDIRECTION);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ tableRedirSource.set(slotValue, NO_REDIRECTION);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ protected void shift(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag, long shiftDelta) {
+ final ByteChunk keyChunk0 = sourceKeyChunks[0].asByteChunk();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final byte k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ tableRedirSource.set(slotValue, mappedRowKey + shiftDelta);
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ tableRedirSource.set(slotValue, mappedRowKey + shiftDelta);
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ protected void modify(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final ByteChunk keyChunk0 = sourceKeyChunks[0].asByteChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final byte k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ private static int hash(byte k0) {
+ int hash = ByteChunkHasher.hashInitialSingle(k0);
+ return hash;
+ }
+
+ private boolean migrateOneLocation(int locationToMigrate) {
+ final int currentStateValue = alternateSlotToOutputRow.getUnsafe(locationToMigrate);
+ if (currentStateValue == EMPTY_OUTPUT_ROW) {
+ return false;
+ }
+ final byte k0 = alternateKeySource0.getUnsafe(locationToMigrate);
+ final int hash = hash(k0);
+ int destinationTableLocation = hashToTableLocation(hash);
+ while (slotToOutputRow.getUnsafe(destinationTableLocation) != EMPTY_OUTPUT_ROW) {
+ destinationTableLocation = nextTableLocation(destinationTableLocation);
+ }
+ mainKeySource0.set(destinationTableLocation, k0);
+ slotToOutputRow.set(destinationTableLocation, currentStateValue);
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(locationToMigrate);
+ mainModifiedTrackerCookieSource.set(destinationTableLocation, cookie);
+ alternateModifiedTrackerCookieSource.set(locationToMigrate, EMPTY_COOKIE_SLOT);
+ alternateSlotToOutputRow.set(locationToMigrate, EMPTY_OUTPUT_ROW);
+ return true;
+ }
+
+ @Override
+ protected int rehashInternalPartial(int entriesToRehash) {
+ int rehashedEntries = 0;
+ while (rehashPointer > 0 && rehashedEntries < entriesToRehash) {
+ if (migrateOneLocation(--rehashPointer)) {
+ rehashedEntries++;
+ }
+ }
+ return rehashedEntries;
+ }
+
+ @Override
+ protected void newAlternate() {
+ super.newAlternate();
+ this.mainKeySource0 = (ImmutableByteArraySource)super.mainKeySources[0];
+ this.alternateKeySource0 = (ImmutableByteArraySource)super.alternateKeySources[0];
+ }
+
+ @Override
+ protected void clearAlternate() {
+ super.clearAlternate();
+ this.alternateKeySource0 = null;
+ }
+
+ @Override
+ protected void migrateFront() {
+ int location = 0;
+ while (migrateOneLocation(location++));
+ }
+
+ @Override
+ protected void rehashInternalFull(final int oldSize) {
+ final byte[] destKeyArray0 = new byte[tableSize];
+ final int[] destState = new int[tableSize];
+ Arrays.fill(destState, EMPTY_OUTPUT_ROW);
+ final byte [] originalKeyArray0 = mainKeySource0.getArray();
+ mainKeySource0.setArray(destKeyArray0);
+ final int [] originalStateArray = slotToOutputRow.getArray();
+ slotToOutputRow.setArray(destState);
+ final long [] oldModifiedCookie = mainModifiedTrackerCookieSource.getArray();
+ final long [] destModifiedCookie = new long[tableSize];
+ mainModifiedTrackerCookieSource.setArray(destModifiedCookie);
+ for (int sourceBucket = 0; sourceBucket < oldSize; ++sourceBucket) {
+ final int currentStateValue = originalStateArray[sourceBucket];
+ if (currentStateValue == EMPTY_OUTPUT_ROW) {
+ continue;
+ }
+ final byte k0 = originalKeyArray0[sourceBucket];
+ final int hash = hash(k0);
+ final int firstDestinationTableLocation = hashToTableLocation(hash);
+ int destinationTableLocation = firstDestinationTableLocation;
+ while (true) {
+ if (destState[destinationTableLocation] == EMPTY_OUTPUT_ROW) {
+ destKeyArray0[destinationTableLocation] = k0;
+ destState[destinationTableLocation] = originalStateArray[sourceBucket];
+ destModifiedCookie[destinationTableLocation] = oldModifiedCookie[sourceBucket];
+ break;
+ }
+ destinationTableLocation = nextTableLocation(destinationTableLocation);
+ Assert.neq(destinationTableLocation, "destinationTableLocation", firstDestinationTableLocation, "firstDestinationTableLocation");
+ }
+ }
+ }
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherChar.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherChar.java
new file mode 100644
index 00000000000..e4929940c0e
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherChar.java
@@ -0,0 +1,346 @@
+// DO NOT EDIT THIS CLASS, AUTOMATICALLY GENERATED BY io.deephaven.engine.table.impl.by.typed.TypedHasherFactory
+// Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.engine.table.impl.multijoin.typed.incopen.gen;
+
+import static io.deephaven.util.compare.CharComparisons.eq;
+
+import io.deephaven.base.verify.Assert;
+import io.deephaven.chunk.CharChunk;
+import io.deephaven.chunk.Chunk;
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.attributes.Values;
+import io.deephaven.chunk.util.hashing.CharChunkHasher;
+import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.OrderedRowKeys;
+import io.deephaven.engine.table.ColumnSource;
+import io.deephaven.engine.table.impl.MultiJoinModifiedSlotTracker;
+import io.deephaven.engine.table.impl.multijoin.IncrementalMultiJoinStateManagerTypedBase;
+import io.deephaven.engine.table.impl.sources.LongArraySource;
+import io.deephaven.engine.table.impl.sources.immutable.ImmutableCharArraySource;
+import java.lang.Override;
+import java.util.Arrays;
+
+final class IncrementalMultiJoinHasherChar extends IncrementalMultiJoinStateManagerTypedBase {
+ private ImmutableCharArraySource mainKeySource0;
+
+ private ImmutableCharArraySource alternateKeySource0;
+
+ public IncrementalMultiJoinHasherChar(ColumnSource[] tableKeySources,
+ ColumnSource[] originalTableKeySources, int tableSize, double maximumLoadFactor,
+ double targetLoadFactor) {
+ super(tableKeySources, originalTableKeySources, tableSize, maximumLoadFactor);
+ this.mainKeySource0 = (ImmutableCharArraySource) super.mainKeySources[0];
+ this.mainKeySource0.ensureCapacity(tableSize);
+ }
+
+ private int nextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (tableSize - 1);
+ }
+
+ private int alternateNextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (alternateTableSize - 1);
+ }
+
+ protected void buildFromTable(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final CharChunk keyChunk0 = sourceKeyChunks[0].asCharChunk();
+ final int chunkSize = keyChunk0.size();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final char k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ int tableLocation = firstTableLocation;
+ MAIN_SEARCH: while (true) {
+ int slotValue = slotToOutputRow.getUnsafe(tableLocation);
+ if (slotValue == EMPTY_OUTPUT_ROW) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ int alternateTableLocation = firstAlternateTableLocation;
+ while (alternateTableLocation < rehashPointer) {
+ slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation);
+ if (slotValue == EMPTY_OUTPUT_ROW) {
+ break;
+ } else if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION) {
+ throw new IllegalStateException("Duplicate key found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition));
+ if (modifiedSlotTracker != null) {
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag));
+ }
+ break MAIN_SEARCH;
+ } else {
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ numEntries++;
+ mainKeySource0.set(tableLocation, k0);
+ final int outputKey = numEntries - 1;
+ slotToOutputRow.set(tableLocation, outputKey);
+ tableRedirSource.set(outputKey, rowKeyChunk.get(chunkPosition));
+ outputKeySources[0].set((long)outputKey, k0);
+ // NOTE: if there are other tables adding this row this cycle, we will add these into the slot
+ // tracker. However, when the modified slots are processed we will identify the output row as new
+ // for this cycle and ignore the incomplete tracker data.
+ mainModifiedTrackerCookieSource.set(tableLocation, EMPTY_COOKIE_SLOT);
+ break;
+ } else if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION) {
+ throw new IllegalStateException("Duplicate key found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition));
+ if (modifiedSlotTracker != null) {
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag));
+ }
+ break;
+ } else {
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ }
+ }
+ }
+
+ protected void remove(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final CharChunk keyChunk0 = sourceKeyChunks[0].asCharChunk();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final char k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ tableRedirSource.set(slotValue, NO_REDIRECTION);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ tableRedirSource.set(slotValue, NO_REDIRECTION);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ protected void shift(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag, long shiftDelta) {
+ final CharChunk keyChunk0 = sourceKeyChunks[0].asCharChunk();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final char k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ tableRedirSource.set(slotValue, mappedRowKey + shiftDelta);
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ tableRedirSource.set(slotValue, mappedRowKey + shiftDelta);
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ protected void modify(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final CharChunk keyChunk0 = sourceKeyChunks[0].asCharChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final char k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ private static int hash(char k0) {
+ int hash = CharChunkHasher.hashInitialSingle(k0);
+ return hash;
+ }
+
+ private boolean migrateOneLocation(int locationToMigrate) {
+ final int currentStateValue = alternateSlotToOutputRow.getUnsafe(locationToMigrate);
+ if (currentStateValue == EMPTY_OUTPUT_ROW) {
+ return false;
+ }
+ final char k0 = alternateKeySource0.getUnsafe(locationToMigrate);
+ final int hash = hash(k0);
+ int destinationTableLocation = hashToTableLocation(hash);
+ while (slotToOutputRow.getUnsafe(destinationTableLocation) != EMPTY_OUTPUT_ROW) {
+ destinationTableLocation = nextTableLocation(destinationTableLocation);
+ }
+ mainKeySource0.set(destinationTableLocation, k0);
+ slotToOutputRow.set(destinationTableLocation, currentStateValue);
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(locationToMigrate);
+ mainModifiedTrackerCookieSource.set(destinationTableLocation, cookie);
+ alternateModifiedTrackerCookieSource.set(locationToMigrate, EMPTY_COOKIE_SLOT);
+ alternateSlotToOutputRow.set(locationToMigrate, EMPTY_OUTPUT_ROW);
+ return true;
+ }
+
+ @Override
+ protected int rehashInternalPartial(int entriesToRehash) {
+ int rehashedEntries = 0;
+ while (rehashPointer > 0 && rehashedEntries < entriesToRehash) {
+ if (migrateOneLocation(--rehashPointer)) {
+ rehashedEntries++;
+ }
+ }
+ return rehashedEntries;
+ }
+
+ @Override
+ protected void newAlternate() {
+ super.newAlternate();
+ this.mainKeySource0 = (ImmutableCharArraySource)super.mainKeySources[0];
+ this.alternateKeySource0 = (ImmutableCharArraySource)super.alternateKeySources[0];
+ }
+
+ @Override
+ protected void clearAlternate() {
+ super.clearAlternate();
+ this.alternateKeySource0 = null;
+ }
+
+ @Override
+ protected void migrateFront() {
+ int location = 0;
+ while (migrateOneLocation(location++));
+ }
+
+ @Override
+ protected void rehashInternalFull(final int oldSize) {
+ final char[] destKeyArray0 = new char[tableSize];
+ final int[] destState = new int[tableSize];
+ Arrays.fill(destState, EMPTY_OUTPUT_ROW);
+ final char [] originalKeyArray0 = mainKeySource0.getArray();
+ mainKeySource0.setArray(destKeyArray0);
+ final int [] originalStateArray = slotToOutputRow.getArray();
+ slotToOutputRow.setArray(destState);
+ final long [] oldModifiedCookie = mainModifiedTrackerCookieSource.getArray();
+ final long [] destModifiedCookie = new long[tableSize];
+ mainModifiedTrackerCookieSource.setArray(destModifiedCookie);
+ for (int sourceBucket = 0; sourceBucket < oldSize; ++sourceBucket) {
+ final int currentStateValue = originalStateArray[sourceBucket];
+ if (currentStateValue == EMPTY_OUTPUT_ROW) {
+ continue;
+ }
+ final char k0 = originalKeyArray0[sourceBucket];
+ final int hash = hash(k0);
+ final int firstDestinationTableLocation = hashToTableLocation(hash);
+ int destinationTableLocation = firstDestinationTableLocation;
+ while (true) {
+ if (destState[destinationTableLocation] == EMPTY_OUTPUT_ROW) {
+ destKeyArray0[destinationTableLocation] = k0;
+ destState[destinationTableLocation] = originalStateArray[sourceBucket];
+ destModifiedCookie[destinationTableLocation] = oldModifiedCookie[sourceBucket];
+ break;
+ }
+ destinationTableLocation = nextTableLocation(destinationTableLocation);
+ Assert.neq(destinationTableLocation, "destinationTableLocation", firstDestinationTableLocation, "firstDestinationTableLocation");
+ }
+ }
+ }
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherDouble.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherDouble.java
new file mode 100644
index 00000000000..ccc988612a6
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherDouble.java
@@ -0,0 +1,346 @@
+// DO NOT EDIT THIS CLASS, AUTOMATICALLY GENERATED BY io.deephaven.engine.table.impl.by.typed.TypedHasherFactory
+// Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.engine.table.impl.multijoin.typed.incopen.gen;
+
+import static io.deephaven.util.compare.DoubleComparisons.eq;
+
+import io.deephaven.base.verify.Assert;
+import io.deephaven.chunk.Chunk;
+import io.deephaven.chunk.DoubleChunk;
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.attributes.Values;
+import io.deephaven.chunk.util.hashing.DoubleChunkHasher;
+import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.OrderedRowKeys;
+import io.deephaven.engine.table.ColumnSource;
+import io.deephaven.engine.table.impl.MultiJoinModifiedSlotTracker;
+import io.deephaven.engine.table.impl.multijoin.IncrementalMultiJoinStateManagerTypedBase;
+import io.deephaven.engine.table.impl.sources.LongArraySource;
+import io.deephaven.engine.table.impl.sources.immutable.ImmutableDoubleArraySource;
+import java.lang.Override;
+import java.util.Arrays;
+
+final class IncrementalMultiJoinHasherDouble extends IncrementalMultiJoinStateManagerTypedBase {
+ private ImmutableDoubleArraySource mainKeySource0;
+
+ private ImmutableDoubleArraySource alternateKeySource0;
+
+ public IncrementalMultiJoinHasherDouble(ColumnSource[] tableKeySources,
+ ColumnSource[] originalTableKeySources, int tableSize, double maximumLoadFactor,
+ double targetLoadFactor) {
+ super(tableKeySources, originalTableKeySources, tableSize, maximumLoadFactor);
+ this.mainKeySource0 = (ImmutableDoubleArraySource) super.mainKeySources[0];
+ this.mainKeySource0.ensureCapacity(tableSize);
+ }
+
+ private int nextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (tableSize - 1);
+ }
+
+ private int alternateNextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (alternateTableSize - 1);
+ }
+
+ protected void buildFromTable(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final DoubleChunk keyChunk0 = sourceKeyChunks[0].asDoubleChunk();
+ final int chunkSize = keyChunk0.size();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final double k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ int tableLocation = firstTableLocation;
+ MAIN_SEARCH: while (true) {
+ int slotValue = slotToOutputRow.getUnsafe(tableLocation);
+ if (slotValue == EMPTY_OUTPUT_ROW) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ int alternateTableLocation = firstAlternateTableLocation;
+ while (alternateTableLocation < rehashPointer) {
+ slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation);
+ if (slotValue == EMPTY_OUTPUT_ROW) {
+ break;
+ } else if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION) {
+ throw new IllegalStateException("Duplicate key found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition));
+ if (modifiedSlotTracker != null) {
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag));
+ }
+ break MAIN_SEARCH;
+ } else {
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ numEntries++;
+ mainKeySource0.set(tableLocation, k0);
+ final int outputKey = numEntries - 1;
+ slotToOutputRow.set(tableLocation, outputKey);
+ tableRedirSource.set(outputKey, rowKeyChunk.get(chunkPosition));
+ outputKeySources[0].set((long)outputKey, k0);
+ // NOTE: if there are other tables adding this row this cycle, we will add these into the slot
+ // tracker. However, when the modified slots are processed we will identify the output row as new
+ // for this cycle and ignore the incomplete tracker data.
+ mainModifiedTrackerCookieSource.set(tableLocation, EMPTY_COOKIE_SLOT);
+ break;
+ } else if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION) {
+ throw new IllegalStateException("Duplicate key found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition));
+ if (modifiedSlotTracker != null) {
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag));
+ }
+ break;
+ } else {
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ }
+ }
+ }
+
+ protected void remove(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final DoubleChunk keyChunk0 = sourceKeyChunks[0].asDoubleChunk();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final double k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ tableRedirSource.set(slotValue, NO_REDIRECTION);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ tableRedirSource.set(slotValue, NO_REDIRECTION);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ protected void shift(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag, long shiftDelta) {
+ final DoubleChunk keyChunk0 = sourceKeyChunks[0].asDoubleChunk();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final double k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ tableRedirSource.set(slotValue, mappedRowKey + shiftDelta);
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ tableRedirSource.set(slotValue, mappedRowKey + shiftDelta);
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ protected void modify(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final DoubleChunk keyChunk0 = sourceKeyChunks[0].asDoubleChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final double k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ private static int hash(double k0) {
+ int hash = DoubleChunkHasher.hashInitialSingle(k0);
+ return hash;
+ }
+
+ private boolean migrateOneLocation(int locationToMigrate) {
+ final int currentStateValue = alternateSlotToOutputRow.getUnsafe(locationToMigrate);
+ if (currentStateValue == EMPTY_OUTPUT_ROW) {
+ return false;
+ }
+ final double k0 = alternateKeySource0.getUnsafe(locationToMigrate);
+ final int hash = hash(k0);
+ int destinationTableLocation = hashToTableLocation(hash);
+ while (slotToOutputRow.getUnsafe(destinationTableLocation) != EMPTY_OUTPUT_ROW) {
+ destinationTableLocation = nextTableLocation(destinationTableLocation);
+ }
+ mainKeySource0.set(destinationTableLocation, k0);
+ slotToOutputRow.set(destinationTableLocation, currentStateValue);
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(locationToMigrate);
+ mainModifiedTrackerCookieSource.set(destinationTableLocation, cookie);
+ alternateModifiedTrackerCookieSource.set(locationToMigrate, EMPTY_COOKIE_SLOT);
+ alternateSlotToOutputRow.set(locationToMigrate, EMPTY_OUTPUT_ROW);
+ return true;
+ }
+
+ @Override
+ protected int rehashInternalPartial(int entriesToRehash) {
+ int rehashedEntries = 0;
+ while (rehashPointer > 0 && rehashedEntries < entriesToRehash) {
+ if (migrateOneLocation(--rehashPointer)) {
+ rehashedEntries++;
+ }
+ }
+ return rehashedEntries;
+ }
+
+ @Override
+ protected void newAlternate() {
+ super.newAlternate();
+ this.mainKeySource0 = (ImmutableDoubleArraySource)super.mainKeySources[0];
+ this.alternateKeySource0 = (ImmutableDoubleArraySource)super.alternateKeySources[0];
+ }
+
+ @Override
+ protected void clearAlternate() {
+ super.clearAlternate();
+ this.alternateKeySource0 = null;
+ }
+
+ @Override
+ protected void migrateFront() {
+ int location = 0;
+ while (migrateOneLocation(location++));
+ }
+
+ @Override
+ protected void rehashInternalFull(final int oldSize) {
+ final double[] destKeyArray0 = new double[tableSize];
+ final int[] destState = new int[tableSize];
+ Arrays.fill(destState, EMPTY_OUTPUT_ROW);
+ final double [] originalKeyArray0 = mainKeySource0.getArray();
+ mainKeySource0.setArray(destKeyArray0);
+ final int [] originalStateArray = slotToOutputRow.getArray();
+ slotToOutputRow.setArray(destState);
+ final long [] oldModifiedCookie = mainModifiedTrackerCookieSource.getArray();
+ final long [] destModifiedCookie = new long[tableSize];
+ mainModifiedTrackerCookieSource.setArray(destModifiedCookie);
+ for (int sourceBucket = 0; sourceBucket < oldSize; ++sourceBucket) {
+ final int currentStateValue = originalStateArray[sourceBucket];
+ if (currentStateValue == EMPTY_OUTPUT_ROW) {
+ continue;
+ }
+ final double k0 = originalKeyArray0[sourceBucket];
+ final int hash = hash(k0);
+ final int firstDestinationTableLocation = hashToTableLocation(hash);
+ int destinationTableLocation = firstDestinationTableLocation;
+ while (true) {
+ if (destState[destinationTableLocation] == EMPTY_OUTPUT_ROW) {
+ destKeyArray0[destinationTableLocation] = k0;
+ destState[destinationTableLocation] = originalStateArray[sourceBucket];
+ destModifiedCookie[destinationTableLocation] = oldModifiedCookie[sourceBucket];
+ break;
+ }
+ destinationTableLocation = nextTableLocation(destinationTableLocation);
+ Assert.neq(destinationTableLocation, "destinationTableLocation", firstDestinationTableLocation, "firstDestinationTableLocation");
+ }
+ }
+ }
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherFloat.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherFloat.java
new file mode 100644
index 00000000000..115a25b0ca8
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherFloat.java
@@ -0,0 +1,346 @@
+// DO NOT EDIT THIS CLASS, AUTOMATICALLY GENERATED BY io.deephaven.engine.table.impl.by.typed.TypedHasherFactory
+// Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.engine.table.impl.multijoin.typed.incopen.gen;
+
+import static io.deephaven.util.compare.FloatComparisons.eq;
+
+import io.deephaven.base.verify.Assert;
+import io.deephaven.chunk.Chunk;
+import io.deephaven.chunk.FloatChunk;
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.attributes.Values;
+import io.deephaven.chunk.util.hashing.FloatChunkHasher;
+import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.OrderedRowKeys;
+import io.deephaven.engine.table.ColumnSource;
+import io.deephaven.engine.table.impl.MultiJoinModifiedSlotTracker;
+import io.deephaven.engine.table.impl.multijoin.IncrementalMultiJoinStateManagerTypedBase;
+import io.deephaven.engine.table.impl.sources.LongArraySource;
+import io.deephaven.engine.table.impl.sources.immutable.ImmutableFloatArraySource;
+import java.lang.Override;
+import java.util.Arrays;
+
+final class IncrementalMultiJoinHasherFloat extends IncrementalMultiJoinStateManagerTypedBase {
+ private ImmutableFloatArraySource mainKeySource0;
+
+ private ImmutableFloatArraySource alternateKeySource0;
+
+ public IncrementalMultiJoinHasherFloat(ColumnSource[] tableKeySources,
+ ColumnSource[] originalTableKeySources, int tableSize, double maximumLoadFactor,
+ double targetLoadFactor) {
+ super(tableKeySources, originalTableKeySources, tableSize, maximumLoadFactor);
+ this.mainKeySource0 = (ImmutableFloatArraySource) super.mainKeySources[0];
+ this.mainKeySource0.ensureCapacity(tableSize);
+ }
+
+ private int nextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (tableSize - 1);
+ }
+
+ private int alternateNextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (alternateTableSize - 1);
+ }
+
+ protected void buildFromTable(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final FloatChunk keyChunk0 = sourceKeyChunks[0].asFloatChunk();
+ final int chunkSize = keyChunk0.size();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final float k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ int tableLocation = firstTableLocation;
+ MAIN_SEARCH: while (true) {
+ int slotValue = slotToOutputRow.getUnsafe(tableLocation);
+ if (slotValue == EMPTY_OUTPUT_ROW) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ int alternateTableLocation = firstAlternateTableLocation;
+ while (alternateTableLocation < rehashPointer) {
+ slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation);
+ if (slotValue == EMPTY_OUTPUT_ROW) {
+ break;
+ } else if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION) {
+ throw new IllegalStateException("Duplicate key found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition));
+ if (modifiedSlotTracker != null) {
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag));
+ }
+ break MAIN_SEARCH;
+ } else {
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ numEntries++;
+ mainKeySource0.set(tableLocation, k0);
+ final int outputKey = numEntries - 1;
+ slotToOutputRow.set(tableLocation, outputKey);
+ tableRedirSource.set(outputKey, rowKeyChunk.get(chunkPosition));
+ outputKeySources[0].set((long)outputKey, k0);
+ // NOTE: if there are other tables adding this row this cycle, we will add these into the slot
+ // tracker. However, when the modified slots are processed we will identify the output row as new
+ // for this cycle and ignore the incomplete tracker data.
+ mainModifiedTrackerCookieSource.set(tableLocation, EMPTY_COOKIE_SLOT);
+ break;
+ } else if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION) {
+ throw new IllegalStateException("Duplicate key found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition));
+ if (modifiedSlotTracker != null) {
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag));
+ }
+ break;
+ } else {
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ }
+ }
+ }
+
+ protected void remove(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final FloatChunk keyChunk0 = sourceKeyChunks[0].asFloatChunk();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final float k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ tableRedirSource.set(slotValue, NO_REDIRECTION);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ tableRedirSource.set(slotValue, NO_REDIRECTION);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ protected void shift(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag, long shiftDelta) {
+ final FloatChunk keyChunk0 = sourceKeyChunks[0].asFloatChunk();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final float k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ tableRedirSource.set(slotValue, mappedRowKey + shiftDelta);
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ tableRedirSource.set(slotValue, mappedRowKey + shiftDelta);
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ protected void modify(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final FloatChunk keyChunk0 = sourceKeyChunks[0].asFloatChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final float k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ private static int hash(float k0) {
+ int hash = FloatChunkHasher.hashInitialSingle(k0);
+ return hash;
+ }
+
+ private boolean migrateOneLocation(int locationToMigrate) {
+ final int currentStateValue = alternateSlotToOutputRow.getUnsafe(locationToMigrate);
+ if (currentStateValue == EMPTY_OUTPUT_ROW) {
+ return false;
+ }
+ final float k0 = alternateKeySource0.getUnsafe(locationToMigrate);
+ final int hash = hash(k0);
+ int destinationTableLocation = hashToTableLocation(hash);
+ while (slotToOutputRow.getUnsafe(destinationTableLocation) != EMPTY_OUTPUT_ROW) {
+ destinationTableLocation = nextTableLocation(destinationTableLocation);
+ }
+ mainKeySource0.set(destinationTableLocation, k0);
+ slotToOutputRow.set(destinationTableLocation, currentStateValue);
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(locationToMigrate);
+ mainModifiedTrackerCookieSource.set(destinationTableLocation, cookie);
+ alternateModifiedTrackerCookieSource.set(locationToMigrate, EMPTY_COOKIE_SLOT);
+ alternateSlotToOutputRow.set(locationToMigrate, EMPTY_OUTPUT_ROW);
+ return true;
+ }
+
+ @Override
+ protected int rehashInternalPartial(int entriesToRehash) {
+ int rehashedEntries = 0;
+ while (rehashPointer > 0 && rehashedEntries < entriesToRehash) {
+ if (migrateOneLocation(--rehashPointer)) {
+ rehashedEntries++;
+ }
+ }
+ return rehashedEntries;
+ }
+
+ @Override
+ protected void newAlternate() {
+ super.newAlternate();
+ this.mainKeySource0 = (ImmutableFloatArraySource)super.mainKeySources[0];
+ this.alternateKeySource0 = (ImmutableFloatArraySource)super.alternateKeySources[0];
+ }
+
+ @Override
+ protected void clearAlternate() {
+ super.clearAlternate();
+ this.alternateKeySource0 = null;
+ }
+
+ @Override
+ protected void migrateFront() {
+ int location = 0;
+ while (migrateOneLocation(location++));
+ }
+
+ @Override
+ protected void rehashInternalFull(final int oldSize) {
+ final float[] destKeyArray0 = new float[tableSize];
+ final int[] destState = new int[tableSize];
+ Arrays.fill(destState, EMPTY_OUTPUT_ROW);
+ final float [] originalKeyArray0 = mainKeySource0.getArray();
+ mainKeySource0.setArray(destKeyArray0);
+ final int [] originalStateArray = slotToOutputRow.getArray();
+ slotToOutputRow.setArray(destState);
+ final long [] oldModifiedCookie = mainModifiedTrackerCookieSource.getArray();
+ final long [] destModifiedCookie = new long[tableSize];
+ mainModifiedTrackerCookieSource.setArray(destModifiedCookie);
+ for (int sourceBucket = 0; sourceBucket < oldSize; ++sourceBucket) {
+ final int currentStateValue = originalStateArray[sourceBucket];
+ if (currentStateValue == EMPTY_OUTPUT_ROW) {
+ continue;
+ }
+ final float k0 = originalKeyArray0[sourceBucket];
+ final int hash = hash(k0);
+ final int firstDestinationTableLocation = hashToTableLocation(hash);
+ int destinationTableLocation = firstDestinationTableLocation;
+ while (true) {
+ if (destState[destinationTableLocation] == EMPTY_OUTPUT_ROW) {
+ destKeyArray0[destinationTableLocation] = k0;
+ destState[destinationTableLocation] = originalStateArray[sourceBucket];
+ destModifiedCookie[destinationTableLocation] = oldModifiedCookie[sourceBucket];
+ break;
+ }
+ destinationTableLocation = nextTableLocation(destinationTableLocation);
+ Assert.neq(destinationTableLocation, "destinationTableLocation", firstDestinationTableLocation, "firstDestinationTableLocation");
+ }
+ }
+ }
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherInt.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherInt.java
new file mode 100644
index 00000000000..9de0920427d
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherInt.java
@@ -0,0 +1,346 @@
+// DO NOT EDIT THIS CLASS, AUTOMATICALLY GENERATED BY io.deephaven.engine.table.impl.by.typed.TypedHasherFactory
+// Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.engine.table.impl.multijoin.typed.incopen.gen;
+
+import static io.deephaven.util.compare.IntComparisons.eq;
+
+import io.deephaven.base.verify.Assert;
+import io.deephaven.chunk.Chunk;
+import io.deephaven.chunk.IntChunk;
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.attributes.Values;
+import io.deephaven.chunk.util.hashing.IntChunkHasher;
+import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.OrderedRowKeys;
+import io.deephaven.engine.table.ColumnSource;
+import io.deephaven.engine.table.impl.MultiJoinModifiedSlotTracker;
+import io.deephaven.engine.table.impl.multijoin.IncrementalMultiJoinStateManagerTypedBase;
+import io.deephaven.engine.table.impl.sources.LongArraySource;
+import io.deephaven.engine.table.impl.sources.immutable.ImmutableIntArraySource;
+import java.lang.Override;
+import java.util.Arrays;
+
+final class IncrementalMultiJoinHasherInt extends IncrementalMultiJoinStateManagerTypedBase {
+ private ImmutableIntArraySource mainKeySource0;
+
+ private ImmutableIntArraySource alternateKeySource0;
+
+ public IncrementalMultiJoinHasherInt(ColumnSource[] tableKeySources,
+ ColumnSource[] originalTableKeySources, int tableSize, double maximumLoadFactor,
+ double targetLoadFactor) {
+ super(tableKeySources, originalTableKeySources, tableSize, maximumLoadFactor);
+ this.mainKeySource0 = (ImmutableIntArraySource) super.mainKeySources[0];
+ this.mainKeySource0.ensureCapacity(tableSize);
+ }
+
+ private int nextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (tableSize - 1);
+ }
+
+ private int alternateNextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (alternateTableSize - 1);
+ }
+
+ protected void buildFromTable(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final IntChunk keyChunk0 = sourceKeyChunks[0].asIntChunk();
+ final int chunkSize = keyChunk0.size();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final int k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ int tableLocation = firstTableLocation;
+ MAIN_SEARCH: while (true) {
+ int slotValue = slotToOutputRow.getUnsafe(tableLocation);
+ if (slotValue == EMPTY_OUTPUT_ROW) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ int alternateTableLocation = firstAlternateTableLocation;
+ while (alternateTableLocation < rehashPointer) {
+ slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation);
+ if (slotValue == EMPTY_OUTPUT_ROW) {
+ break;
+ } else if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION) {
+ throw new IllegalStateException("Duplicate key found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition));
+ if (modifiedSlotTracker != null) {
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag));
+ }
+ break MAIN_SEARCH;
+ } else {
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ numEntries++;
+ mainKeySource0.set(tableLocation, k0);
+ final int outputKey = numEntries - 1;
+ slotToOutputRow.set(tableLocation, outputKey);
+ tableRedirSource.set(outputKey, rowKeyChunk.get(chunkPosition));
+ outputKeySources[0].set((long)outputKey, k0);
+ // NOTE: if there are other tables adding this row this cycle, we will add these into the slot
+ // tracker. However, when the modified slots are processed we will identify the output row as new
+ // for this cycle and ignore the incomplete tracker data.
+ mainModifiedTrackerCookieSource.set(tableLocation, EMPTY_COOKIE_SLOT);
+ break;
+ } else if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION) {
+ throw new IllegalStateException("Duplicate key found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition));
+ if (modifiedSlotTracker != null) {
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag));
+ }
+ break;
+ } else {
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ }
+ }
+ }
+
+ protected void remove(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final IntChunk keyChunk0 = sourceKeyChunks[0].asIntChunk();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final int k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ tableRedirSource.set(slotValue, NO_REDIRECTION);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ tableRedirSource.set(slotValue, NO_REDIRECTION);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ protected void shift(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag, long shiftDelta) {
+ final IntChunk keyChunk0 = sourceKeyChunks[0].asIntChunk();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final int k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ tableRedirSource.set(slotValue, mappedRowKey + shiftDelta);
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ tableRedirSource.set(slotValue, mappedRowKey + shiftDelta);
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ protected void modify(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final IntChunk keyChunk0 = sourceKeyChunks[0].asIntChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final int k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ private static int hash(int k0) {
+ int hash = IntChunkHasher.hashInitialSingle(k0);
+ return hash;
+ }
+
+ private boolean migrateOneLocation(int locationToMigrate) {
+ final int currentStateValue = alternateSlotToOutputRow.getUnsafe(locationToMigrate);
+ if (currentStateValue == EMPTY_OUTPUT_ROW) {
+ return false;
+ }
+ final int k0 = alternateKeySource0.getUnsafe(locationToMigrate);
+ final int hash = hash(k0);
+ int destinationTableLocation = hashToTableLocation(hash);
+ while (slotToOutputRow.getUnsafe(destinationTableLocation) != EMPTY_OUTPUT_ROW) {
+ destinationTableLocation = nextTableLocation(destinationTableLocation);
+ }
+ mainKeySource0.set(destinationTableLocation, k0);
+ slotToOutputRow.set(destinationTableLocation, currentStateValue);
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(locationToMigrate);
+ mainModifiedTrackerCookieSource.set(destinationTableLocation, cookie);
+ alternateModifiedTrackerCookieSource.set(locationToMigrate, EMPTY_COOKIE_SLOT);
+ alternateSlotToOutputRow.set(locationToMigrate, EMPTY_OUTPUT_ROW);
+ return true;
+ }
+
+ @Override
+ protected int rehashInternalPartial(int entriesToRehash) {
+ int rehashedEntries = 0;
+ while (rehashPointer > 0 && rehashedEntries < entriesToRehash) {
+ if (migrateOneLocation(--rehashPointer)) {
+ rehashedEntries++;
+ }
+ }
+ return rehashedEntries;
+ }
+
+ @Override
+ protected void newAlternate() {
+ super.newAlternate();
+ this.mainKeySource0 = (ImmutableIntArraySource)super.mainKeySources[0];
+ this.alternateKeySource0 = (ImmutableIntArraySource)super.alternateKeySources[0];
+ }
+
+ @Override
+ protected void clearAlternate() {
+ super.clearAlternate();
+ this.alternateKeySource0 = null;
+ }
+
+ @Override
+ protected void migrateFront() {
+ int location = 0;
+ while (migrateOneLocation(location++));
+ }
+
+ @Override
+ protected void rehashInternalFull(final int oldSize) {
+ final int[] destKeyArray0 = new int[tableSize];
+ final int[] destState = new int[tableSize];
+ Arrays.fill(destState, EMPTY_OUTPUT_ROW);
+ final int [] originalKeyArray0 = mainKeySource0.getArray();
+ mainKeySource0.setArray(destKeyArray0);
+ final int [] originalStateArray = slotToOutputRow.getArray();
+ slotToOutputRow.setArray(destState);
+ final long [] oldModifiedCookie = mainModifiedTrackerCookieSource.getArray();
+ final long [] destModifiedCookie = new long[tableSize];
+ mainModifiedTrackerCookieSource.setArray(destModifiedCookie);
+ for (int sourceBucket = 0; sourceBucket < oldSize; ++sourceBucket) {
+ final int currentStateValue = originalStateArray[sourceBucket];
+ if (currentStateValue == EMPTY_OUTPUT_ROW) {
+ continue;
+ }
+ final int k0 = originalKeyArray0[sourceBucket];
+ final int hash = hash(k0);
+ final int firstDestinationTableLocation = hashToTableLocation(hash);
+ int destinationTableLocation = firstDestinationTableLocation;
+ while (true) {
+ if (destState[destinationTableLocation] == EMPTY_OUTPUT_ROW) {
+ destKeyArray0[destinationTableLocation] = k0;
+ destState[destinationTableLocation] = originalStateArray[sourceBucket];
+ destModifiedCookie[destinationTableLocation] = oldModifiedCookie[sourceBucket];
+ break;
+ }
+ destinationTableLocation = nextTableLocation(destinationTableLocation);
+ Assert.neq(destinationTableLocation, "destinationTableLocation", firstDestinationTableLocation, "firstDestinationTableLocation");
+ }
+ }
+ }
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherLong.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherLong.java
new file mode 100644
index 00000000000..88fe4e58f74
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherLong.java
@@ -0,0 +1,345 @@
+// DO NOT EDIT THIS CLASS, AUTOMATICALLY GENERATED BY io.deephaven.engine.table.impl.by.typed.TypedHasherFactory
+// Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.engine.table.impl.multijoin.typed.incopen.gen;
+
+import static io.deephaven.util.compare.LongComparisons.eq;
+
+import io.deephaven.base.verify.Assert;
+import io.deephaven.chunk.Chunk;
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.attributes.Values;
+import io.deephaven.chunk.util.hashing.LongChunkHasher;
+import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.OrderedRowKeys;
+import io.deephaven.engine.table.ColumnSource;
+import io.deephaven.engine.table.impl.MultiJoinModifiedSlotTracker;
+import io.deephaven.engine.table.impl.multijoin.IncrementalMultiJoinStateManagerTypedBase;
+import io.deephaven.engine.table.impl.sources.LongArraySource;
+import io.deephaven.engine.table.impl.sources.immutable.ImmutableLongArraySource;
+import java.lang.Override;
+import java.util.Arrays;
+
+final class IncrementalMultiJoinHasherLong extends IncrementalMultiJoinStateManagerTypedBase {
+ private ImmutableLongArraySource mainKeySource0;
+
+ private ImmutableLongArraySource alternateKeySource0;
+
+ public IncrementalMultiJoinHasherLong(ColumnSource[] tableKeySources,
+ ColumnSource[] originalTableKeySources, int tableSize, double maximumLoadFactor,
+ double targetLoadFactor) {
+ super(tableKeySources, originalTableKeySources, tableSize, maximumLoadFactor);
+ this.mainKeySource0 = (ImmutableLongArraySource) super.mainKeySources[0];
+ this.mainKeySource0.ensureCapacity(tableSize);
+ }
+
+ private int nextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (tableSize - 1);
+ }
+
+ private int alternateNextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (alternateTableSize - 1);
+ }
+
+ protected void buildFromTable(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final LongChunk keyChunk0 = sourceKeyChunks[0].asLongChunk();
+ final int chunkSize = keyChunk0.size();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final long k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ int tableLocation = firstTableLocation;
+ MAIN_SEARCH: while (true) {
+ int slotValue = slotToOutputRow.getUnsafe(tableLocation);
+ if (slotValue == EMPTY_OUTPUT_ROW) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ int alternateTableLocation = firstAlternateTableLocation;
+ while (alternateTableLocation < rehashPointer) {
+ slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation);
+ if (slotValue == EMPTY_OUTPUT_ROW) {
+ break;
+ } else if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION) {
+ throw new IllegalStateException("Duplicate key found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition));
+ if (modifiedSlotTracker != null) {
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag));
+ }
+ break MAIN_SEARCH;
+ } else {
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ numEntries++;
+ mainKeySource0.set(tableLocation, k0);
+ final int outputKey = numEntries - 1;
+ slotToOutputRow.set(tableLocation, outputKey);
+ tableRedirSource.set(outputKey, rowKeyChunk.get(chunkPosition));
+ outputKeySources[0].set((long)outputKey, k0);
+ // NOTE: if there are other tables adding this row this cycle, we will add these into the slot
+ // tracker. However, when the modified slots are processed we will identify the output row as new
+ // for this cycle and ignore the incomplete tracker data.
+ mainModifiedTrackerCookieSource.set(tableLocation, EMPTY_COOKIE_SLOT);
+ break;
+ } else if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ if (tableRedirSource.getLong(slotValue) != NO_REDIRECTION) {
+ throw new IllegalStateException("Duplicate key found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ tableRedirSource.set(slotValue, rowKeyChunk.get(chunkPosition));
+ if (modifiedSlotTracker != null) {
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, RowSequence.NULL_ROW_KEY, trackerFlag));
+ }
+ break;
+ } else {
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ }
+ }
+ }
+
+ protected void remove(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final LongChunk keyChunk0 = sourceKeyChunks[0].asLongChunk();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final long k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ tableRedirSource.set(slotValue, NO_REDIRECTION);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ tableRedirSource.set(slotValue, NO_REDIRECTION);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ protected void shift(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag, long shiftDelta) {
+ final LongChunk keyChunk0 = sourceKeyChunks[0].asLongChunk();
+ final LongChunk rowKeyChunk = rowSequence.asRowKeyChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final long k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ tableRedirSource.set(slotValue, mappedRowKey + shiftDelta);
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long mappedRowKey = tableRedirSource.getUnsafe(slotValue);
+ Assert.eq(rowKeyChunk.get(chunkPosition), "rowKey", mappedRowKey, "mappedRowKey");
+ tableRedirSource.set(slotValue, mappedRowKey + shiftDelta);
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.addSlot(cookie, slotValue, tableNumber, mappedRowKey, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ protected void modify(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final LongChunk keyChunk0 = sourceKeyChunks[0].asLongChunk();
+ final int chunkSize = keyChunk0.size();
+ for (int chunkPosition = 0; chunkPosition < chunkSize; ++chunkPosition) {
+ final long k0 = keyChunk0.get(chunkPosition);
+ final int hash = hash(k0);
+ final int firstTableLocation = hashToTableLocation(hash);
+ boolean found = false;
+ int tableLocation = firstTableLocation;
+ int slotValue;
+ while ((slotValue = slotToOutputRow.getUnsafe(tableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(mainKeySource0.getUnsafe(tableLocation), k0)) {
+ final long cookie = mainModifiedTrackerCookieSource.getUnsafe(tableLocation);
+ mainModifiedTrackerCookieSource.set(tableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag));
+ found = true;
+ break;
+ }
+ tableLocation = nextTableLocation(tableLocation);
+ Assert.neq(tableLocation, "tableLocation", firstTableLocation, "firstTableLocation");
+ }
+ if (!found) {
+ final int firstAlternateTableLocation = hashToTableLocationAlternate(hash);
+ boolean alternateFound = false;
+ if (firstAlternateTableLocation < rehashPointer) {
+ int alternateTableLocation = firstAlternateTableLocation;
+ while ((slotValue = alternateSlotToOutputRow.getUnsafe(alternateTableLocation)) != EMPTY_OUTPUT_ROW) {
+ if (eq(alternateKeySource0.getUnsafe(alternateTableLocation), k0)) {
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(alternateTableLocation);
+ alternateModifiedTrackerCookieSource.set(alternateTableLocation, modifiedSlotTracker.modifySlot(cookie, slotValue, tableNumber, trackerFlag));
+ alternateFound = true;
+ break;
+ }
+ alternateTableLocation = alternateNextTableLocation(alternateTableLocation);
+ Assert.neq(alternateTableLocation, "alternateTableLocation", firstAlternateTableLocation, "firstAlternateTableLocation");
+ }
+ }
+ if (!alternateFound) {
+ throw new IllegalStateException("Matching row not found for " + keyString(sourceKeyChunks, chunkPosition) + " in table " + tableNumber + ".");
+ }
+ }
+ }
+ }
+
+ private static int hash(long k0) {
+ int hash = LongChunkHasher.hashInitialSingle(k0);
+ return hash;
+ }
+
+ private boolean migrateOneLocation(int locationToMigrate) {
+ final int currentStateValue = alternateSlotToOutputRow.getUnsafe(locationToMigrate);
+ if (currentStateValue == EMPTY_OUTPUT_ROW) {
+ return false;
+ }
+ final long k0 = alternateKeySource0.getUnsafe(locationToMigrate);
+ final int hash = hash(k0);
+ int destinationTableLocation = hashToTableLocation(hash);
+ while (slotToOutputRow.getUnsafe(destinationTableLocation) != EMPTY_OUTPUT_ROW) {
+ destinationTableLocation = nextTableLocation(destinationTableLocation);
+ }
+ mainKeySource0.set(destinationTableLocation, k0);
+ slotToOutputRow.set(destinationTableLocation, currentStateValue);
+ final long cookie = alternateModifiedTrackerCookieSource.getUnsafe(locationToMigrate);
+ mainModifiedTrackerCookieSource.set(destinationTableLocation, cookie);
+ alternateModifiedTrackerCookieSource.set(locationToMigrate, EMPTY_COOKIE_SLOT);
+ alternateSlotToOutputRow.set(locationToMigrate, EMPTY_OUTPUT_ROW);
+ return true;
+ }
+
+ @Override
+ protected int rehashInternalPartial(int entriesToRehash) {
+ int rehashedEntries = 0;
+ while (rehashPointer > 0 && rehashedEntries < entriesToRehash) {
+ if (migrateOneLocation(--rehashPointer)) {
+ rehashedEntries++;
+ }
+ }
+ return rehashedEntries;
+ }
+
+ @Override
+ protected void newAlternate() {
+ super.newAlternate();
+ this.mainKeySource0 = (ImmutableLongArraySource)super.mainKeySources[0];
+ this.alternateKeySource0 = (ImmutableLongArraySource)super.alternateKeySources[0];
+ }
+
+ @Override
+ protected void clearAlternate() {
+ super.clearAlternate();
+ this.alternateKeySource0 = null;
+ }
+
+ @Override
+ protected void migrateFront() {
+ int location = 0;
+ while (migrateOneLocation(location++));
+ }
+
+ @Override
+ protected void rehashInternalFull(final int oldSize) {
+ final long[] destKeyArray0 = new long[tableSize];
+ final int[] destState = new int[tableSize];
+ Arrays.fill(destState, EMPTY_OUTPUT_ROW);
+ final long [] originalKeyArray0 = mainKeySource0.getArray();
+ mainKeySource0.setArray(destKeyArray0);
+ final int [] originalStateArray = slotToOutputRow.getArray();
+ slotToOutputRow.setArray(destState);
+ final long [] oldModifiedCookie = mainModifiedTrackerCookieSource.getArray();
+ final long [] destModifiedCookie = new long[tableSize];
+ mainModifiedTrackerCookieSource.setArray(destModifiedCookie);
+ for (int sourceBucket = 0; sourceBucket < oldSize; ++sourceBucket) {
+ final int currentStateValue = originalStateArray[sourceBucket];
+ if (currentStateValue == EMPTY_OUTPUT_ROW) {
+ continue;
+ }
+ final long k0 = originalKeyArray0[sourceBucket];
+ final int hash = hash(k0);
+ final int firstDestinationTableLocation = hashToTableLocation(hash);
+ int destinationTableLocation = firstDestinationTableLocation;
+ while (true) {
+ if (destState[destinationTableLocation] == EMPTY_OUTPUT_ROW) {
+ destKeyArray0[destinationTableLocation] = k0;
+ destState[destinationTableLocation] = originalStateArray[sourceBucket];
+ destModifiedCookie[destinationTableLocation] = oldModifiedCookie[sourceBucket];
+ break;
+ }
+ destinationTableLocation = nextTableLocation(destinationTableLocation);
+ Assert.neq(destinationTableLocation, "destinationTableLocation", firstDestinationTableLocation, "firstDestinationTableLocation");
+ }
+ }
+ }
+}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherObject.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherObject.java
new file mode 100644
index 00000000000..eda05056088
--- /dev/null
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/multijoin/typed/incopen/gen/IncrementalMultiJoinHasherObject.java
@@ -0,0 +1,348 @@
+// DO NOT EDIT THIS CLASS, AUTOMATICALLY GENERATED BY io.deephaven.engine.table.impl.by.typed.TypedHasherFactory
+// Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.engine.table.impl.multijoin.typed.incopen.gen;
+
+import static io.deephaven.util.compare.ObjectComparisons.eq;
+
+import io.deephaven.base.verify.Assert;
+import io.deephaven.chunk.Chunk;
+import io.deephaven.chunk.LongChunk;
+import io.deephaven.chunk.ObjectChunk;
+import io.deephaven.chunk.attributes.Values;
+import io.deephaven.chunk.util.hashing.ObjectChunkHasher;
+import io.deephaven.engine.rowset.RowSequence;
+import io.deephaven.engine.rowset.chunkattributes.OrderedRowKeys;
+import io.deephaven.engine.table.ColumnSource;
+import io.deephaven.engine.table.impl.MultiJoinModifiedSlotTracker;
+import io.deephaven.engine.table.impl.multijoin.IncrementalMultiJoinStateManagerTypedBase;
+import io.deephaven.engine.table.impl.sources.LongArraySource;
+import io.deephaven.engine.table.impl.sources.immutable.ImmutableObjectArraySource;
+import java.lang.Object;
+import java.lang.Override;
+import java.util.Arrays;
+
+final class IncrementalMultiJoinHasherObject extends IncrementalMultiJoinStateManagerTypedBase {
+ private ImmutableObjectArraySource mainKeySource0;
+
+ private ImmutableObjectArraySource alternateKeySource0;
+
+ public IncrementalMultiJoinHasherObject(ColumnSource[] tableKeySources,
+ ColumnSource[] originalTableKeySources, int tableSize, double maximumLoadFactor,
+ double targetLoadFactor) {
+ super(tableKeySources, originalTableKeySources, tableSize, maximumLoadFactor);
+ this.mainKeySource0 = (ImmutableObjectArraySource) super.mainKeySources[0];
+ this.mainKeySource0.ensureCapacity(tableSize);
+ }
+
+ private int nextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (tableSize - 1);
+ }
+
+ private int alternateNextTableLocation(int tableLocation) {
+ return (tableLocation + 1) & (alternateTableSize - 1);
+ }
+
+ protected void buildFromTable(RowSequence rowSequence, Chunk[] sourceKeyChunks,
+ LongArraySource tableRedirSource, int tableNumber,
+ MultiJoinModifiedSlotTracker modifiedSlotTracker, byte trackerFlag) {
+ final ObjectChunk