keyRetriever, Supplier> downstreamIndexerSupplier) {
+ this.keyRetriever = Objects.requireNonNull(keyRetriever);
this.downstreamIndexerSupplier = Objects.requireNonNull(downstreamIndexerSupplier);
}
diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java
index bc0064c2eb..0088909872 100644
--- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java
+++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java
@@ -12,12 +12,16 @@
* An indexer for entity or fact {@code X},
* maps a property or a combination of properties of {@code X}, denoted by {@code compositeKey},
* to all instances of {@code X} that match those properties,
- * depending on the the indexer type (equal, lower than, ...).
+ * depending on the indexer type (equal, lower than, contain, ...).
* For example for {@code {Lesson(id=1, room=A), Lesson(id=2, room=B), Lesson(id=3, room=A)}},
* calling {@code visit(room=A)} would visit lesson 1 and 3.
*
* The fact X is wrapped in a Tuple, because the {@link TupleState} is needed by clients of
* {@link #forEach(Object, Consumer)}.
+ *
+ * Some indexer types (such as contain, containedIn, ...) have two different key types (modify key vs query key),
+ * depending on the operation type (modify operation vs query operation).
+ * For example, for a contain indexer the modify key is a collection, but the query key is not.
*
* @param The element type. Often a tuple.
* For example for {@code from(A).join(B)}, the tuple is {@code UniTuple} xor {@code UniTuple}.
@@ -25,16 +29,44 @@
*/
@NullMarked
public sealed interface Indexer
- permits ComparisonIndexer, EqualsIndexer, IndexerBackend {
+ permits EqualIndexer, ComparisonIndexer, ContainIndexer, ContainedInIndexer, ContainAnyIndexer, IndexerBackend {
- ListEntry put(Object compositeKey, T tuple);
+ /**
+ * Modify operation.
+ *
+ * @param modifyCompositeKey modify composite key, never null
+ * @param tuple never null
+ * @return the entry to allow remove it from the index directly, never null
+ */
+ ListEntry put(Object modifyCompositeKey, T tuple);
- void remove(Object compositeKey, ListEntry entry);
+ /**
+ * Modify operation.
+ *
+ * @param modifyCompositeKey modify composite key, never null
+ * @param entry never null
+ */
+ void remove(Object modifyCompositeKey, ListEntry entry);
- int size(Object compositeKey);
+ /**
+ * Query operation.
+ *
+ * @param queryCompositeKey query composite key, never null
+ * @return at least 0
+ */
+ int size(Object queryCompositeKey);
- void forEach(Object compositeKey, Consumer tupleConsumer);
+ /**
+ * Query operation.
+ *
+ * @param queryCompositeKey query composite key, never null
+ * @param tupleConsumer never null
+ */
+ void forEach(Object queryCompositeKey, Consumer tupleConsumer);
+ /**
+ * @return true if empty
+ */
boolean isEmpty();
/**
@@ -43,10 +75,10 @@ public sealed interface Indexer
* If the index is modified, a new instance of this list must be retrieved;
* the previous instance is no longer valid and its behavior is undefined.
*
- * @param compositeKey the composite key
+ * @param queryCompositeKey query composite key, never null
* @return all entries for a given composite key;
* the caller must not modify the list
*/
- List extends ListEntry> asList(Object compositeKey);
+ List extends ListEntry> asList(Object queryCompositeKey);
}
diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java
index b30cc7960a..a0505bf6f6 100644
--- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java
+++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java
@@ -79,6 +79,8 @@ public final class IndexerFactory {
public IndexerFactory(AbstractJoiner joiner) {
this.joiner = joiner;
+ // TODO Code encapsulation: remove the field requiresRandomAccess and call joiner.requireRandomAccess() instead?
+ // TODO It also impacts the flip(). Is requiresRandomAccess a good name?
this.requiresRandomAccess = joiner instanceof DefaultBiEnumeratingJoiner, Right_>;
var joinerCount = joiner.getJoinerCount();
if (joinerCount < 2) {
@@ -497,48 +499,63 @@ public Indexer buildIndexer(boolean isLeftBridge) {
return backendSupplier.get();
} else if (joiner.getJoinerCount() == 1) { // Single joiner maps directly to EqualsIndexer or ComparisonIndexer.
var joinerType = joiner.getJoinerType(0);
- if (joinerType == JoinerType.EQUAL) {
- return new EqualsIndexer<>(backendSupplier);
- } else {
- // Note that if creating indexer for a right bridge node, the joiner type has to be flipped.
- // ( becomes .)
- // This does not apply if random access is required,
- // because in that case we create a right bridge only,
- // and we query it from the left.
- var actualJoinerType = isLeftBridge || requiresRandomAccess ? joinerType : joinerType.flip();
- return new ComparisonIndexer<>(actualJoinerType, backendSupplier);
- }
+ KeyRetriever> keyRetriever = new SingleKeyRetriever<>();
+ return buildIndexerPart(isLeftBridge, joinerType, keyRetriever, backendSupplier);
}
- // The following code builds the children first, so it needs to iterate over the joiners in reverse order.
+ // The following code builds the children first, so it iterates over the joiners in reverse order.
var descendingJoinerTypeMap = joinerTypeMap.descendingMap();
Supplier> downstreamIndexerSupplier = backendSupplier;
var indexPropertyId = descendingJoinerTypeMap.size() - 1;
for (var entry : descendingJoinerTypeMap.entrySet()) {
var joinerType = entry.getValue();
if (downstreamIndexerSupplier == backendSupplier && indexPropertyId == 0) {
- if (joinerType == JoinerType.EQUAL) {
- downstreamIndexerSupplier = () -> new EqualsIndexer<>(backendSupplier);
- } else {
- var actualJoinerType = isLeftBridge ? joinerType : joinerType.flip();
- downstreamIndexerSupplier = () -> new ComparisonIndexer<>(actualJoinerType, backendSupplier);
- }
+ KeyRetriever> keyRetriever = new SingleKeyRetriever<>();
+ downstreamIndexerSupplier = () -> buildIndexerPart(isLeftBridge, joinerType, keyRetriever, backendSupplier);
} else {
+ KeyRetriever> keyRetriever = new CompositeKeyRetriever<>(indexPropertyId);
var actualDownstreamIndexerSupplier = downstreamIndexerSupplier;
- var effectivelyFinalIndexPropertyId = indexPropertyId;
- if (joinerType == JoinerType.EQUAL) {
- downstreamIndexerSupplier =
- () -> new EqualsIndexer<>(effectivelyFinalIndexPropertyId, actualDownstreamIndexerSupplier);
- } else {
- var actualJoinerType = isLeftBridge ? joinerType : joinerType.flip();
- downstreamIndexerSupplier = () -> new ComparisonIndexer<>(actualJoinerType, effectivelyFinalIndexPropertyId,
- actualDownstreamIndexerSupplier);
- }
+ downstreamIndexerSupplier = () -> buildIndexerPart(isLeftBridge, joinerType, keyRetriever,
+ actualDownstreamIndexerSupplier);
}
indexPropertyId--;
}
return downstreamIndexerSupplier.get();
}
+ private Indexer buildIndexerPart(boolean isLeftBridge, JoinerType joinerType, KeyRetriever> keyRetriever,
+ Supplier> downstreamIndexerSupplier) {
+ // Note that if creating indexer for a right bridge node, the joiner type has to be flipped.
+ // ( becomes .)
+ // TODO Does the requiresRandomAccess check make sense?
+ // Shouldn't a right bridge always flip, even if there is no left bridge?
+ // TODO For neighborhoods, why create a left bridge index and keep it up to date at all?
+ // This does not apply if random access is required,
+ // because in that case we create a right bridge only,
+ // and we query it from the left.
+ if (!isLeftBridge && !requiresRandomAccess) {
+ joinerType = joinerType.flip();
+ }
+ switch (joinerType) {
+ case EQUAL -> {
+ return new EqualIndexer<>(keyRetriever, downstreamIndexerSupplier);
+ }
+ case LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL -> {
+ return new ComparisonIndexer<>(joinerType, keyRetriever, downstreamIndexerSupplier);
+ }
+ case CONTAIN -> {
+ return new ContainIndexer<>(keyRetriever, downstreamIndexerSupplier);
+ }
+ case CONTAINED_IN -> {
+ return new ContainedInIndexer<>(keyRetriever, downstreamIndexerSupplier);
+ }
+ case CONTAIN_ANY -> {
+ return new ContainAnyIndexer<>(keyRetriever, downstreamIndexerSupplier);
+ }
+ default -> throw new IllegalStateException(
+ "Impossible state: The joiner type (" + joinerType + ") is not implemented.");
+ }
+ }
+
/**
* Represents a function which extracts index keys from a tuple.
*
diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/KeyRetriever.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/KeyRetriever.java
index 233185b2a3..b86f1d382f 100644
--- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/KeyRetriever.java
+++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/KeyRetriever.java
@@ -2,6 +2,14 @@
import java.util.function.Function;
+// TODO Naming confusion: KeyRetriever and KeyExtractor are too similar for separate concepts.
+/**
+ * A function that retrieves keys of a composite key for an {@link Indexer}.
+ * For example, {@code join(..., equals(), lessThan(), greaterThan())} has 3 keys.
+ * Given {@code ("a", 7, 9)} the key retriever for {@code lessThan()} retrieves {@code 7}.
+ *
+ * @param
+ */
sealed interface KeyRetriever extends Function