|
10 | 10 | import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; |
11 | 11 | import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; |
12 | 12 |
|
13 | | -import org.jspecify.annotations.Nullable; |
14 | | - |
15 | 13 | /** |
16 | 14 | * This class has two direct children: {@link AbstractIndexedJoinNode} and {@link AbstractUnindexedJoinNode}. |
17 | 15 | * The logic in either is identical, except that the latter removes all indexing work. |
@@ -92,13 +90,50 @@ protected final void innerUpdateLeft(LeftTuple_ leftTuple, Consumer<Consumer<Uni |
92 | 90 | if (!isFiltering) { |
93 | 91 | outTupleSetLeft.forEach(outTuple -> updateOutTupleLeft(outTuple, leftTuple)); |
94 | 92 | } else { |
95 | | - rightTupleConsumer.accept(rightTuple -> { |
96 | | - IndexedSet<OutTuple_> outTupleSetRight = rightTuple.getStore(inputStoreIndexRightOutTupleSet); |
97 | | - processOutTupleUpdate(leftTuple, rightTuple, outTupleSetRight, outTupleSetLeft, outputStoreIndexRightOutSet); |
98 | | - }); |
| 93 | + if (!leftTuple.state.isActive()) { |
| 94 | + // Assume the following scenario: |
| 95 | + // - The join is of two entities of the same type, both filtering out unassigned. |
| 96 | + // - One entity became unassigned, so the outTuple is getting retracted. |
| 97 | + // - The other entity is still assigned and is being updated. |
| 98 | + // |
| 99 | + // This means the filter would be called with (unassignedEntity, assignedEntity), |
| 100 | + // which breaks the expectation that the filter is only called on two assigned entities |
| 101 | + // and requires adding null checks to the filter for something that should intuitively be impossible. |
| 102 | + // We avoid this situation as it is clear that the outTuple must be retracted anyway, |
| 103 | + // and therefore any further updates to it are pointless. |
| 104 | + // |
| 105 | + // It is possible that the same problem would exist coming from the other side as well, |
| 106 | + // and therefore the right tuple would have to be checked for active state as well. |
| 107 | + // However, no such issue could have been reproduced; when in doubt, leave it out. |
| 108 | + return; |
| 109 | + } |
| 110 | + rightTupleConsumer.accept(rightTuple -> findAndProcessOutTuple(leftTuple, rightTuple, outTupleSetLeft, |
| 111 | + rightTuple.getStore(inputStoreIndexRightOutTupleSet), outputStoreIndexRightOutSet)); |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + private void findAndProcessOutTuple(LeftTuple_ leftTuple, UniTuple<Right_> rightTuple, IndexedSet<OutTuple_> sourceSet, |
| 116 | + IndexedSet<OutTuple_> referenceSet, int outputStoreIndex) { |
| 117 | + var outTuple = findOutTuple(sourceSet, referenceSet, outputStoreIndex); |
| 118 | + if (testFiltering(leftTuple, rightTuple)) { |
| 119 | + if (outTuple == null) { |
| 120 | + insertOutTuple(leftTuple, rightTuple); |
| 121 | + } else { |
| 122 | + updateOutTupleLeft(outTuple, leftTuple); |
| 123 | + } |
| 124 | + } else { |
| 125 | + if (outTuple != null) { |
| 126 | + retractOutTuple(outTuple); |
| 127 | + } |
99 | 128 | } |
100 | 129 | } |
101 | 130 |
|
| 131 | + private static <OutTuple_ extends AbstractTuple> OutTuple_ findOutTuple(IndexedSet<OutTuple_> sourceSet, |
| 132 | + IndexedSet<OutTuple_> referenceSet, int outputStoreIndex) { |
| 133 | + // Hack: outTuple has no left/right input tuple reference, use the left/right outSet reference instead. |
| 134 | + return sourceSet.findFirst(tuple -> referenceSet == tuple.getStore(outputStoreIndex)); |
| 135 | + } |
| 136 | + |
102 | 137 | private void updateOutTupleLeft(OutTuple_ outTuple, LeftTuple_ leftTuple) { |
103 | 138 | setOutTupleLeftFacts(outTuple, leftTuple); |
104 | 139 | doUpdateOutTuple(outTuple); |
@@ -126,52 +161,29 @@ protected final void innerUpdateRight(UniTuple<Right_> rightTuple, Consumer<Cons |
126 | 161 | }); |
127 | 162 | } else { |
128 | 163 | leftTupleConsumer.accept(leftTuple -> { |
129 | | - IndexedSet<OutTuple_> outTupleSetLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleSet); |
130 | | - processOutTupleUpdate(leftTuple, rightTuple, outTupleSetLeft, outTupleSetRight, outputStoreIndexLeftOutSet); |
| 164 | + if (!leftTuple.state.isActive()) { |
| 165 | + // Assume the following scenario: |
| 166 | + // - The join is of two entities of the same type, both filtering out unassigned. |
| 167 | + // - One entity became unassigned, so the outTuple is getting retracted. |
| 168 | + // - The other entity is still assigned and is being updated. |
| 169 | + // |
| 170 | + // This means the filter would be called with (unassignedEntity, assignedEntity), |
| 171 | + // which breaks the expectation that the filter is only called on two assigned entities |
| 172 | + // and requires adding null checks to the filter for something that should intuitively be impossible. |
| 173 | + // We avoid this situation as it is clear that the outTuple must be retracted anyway, |
| 174 | + // and therefore any further updates to it are pointless. |
| 175 | + // |
| 176 | + // It is possible that the same problem would exist coming from the other side as well, |
| 177 | + // and therefore the right tuple would have to be checked for active state as well. |
| 178 | + // However, no such issue could have been reproduced; when in doubt, leave it out. |
| 179 | + return; |
| 180 | + } |
| 181 | + findAndProcessOutTuple(leftTuple, rightTuple, outTupleSetRight, |
| 182 | + leftTuple.getStore(inputStoreIndexLeftOutTupleSet), outputStoreIndexLeftOutSet); |
131 | 183 | }); |
132 | 184 | } |
133 | 185 | } |
134 | 186 |
|
135 | | - private void processOutTupleUpdate(LeftTuple_ leftTuple, UniTuple<Right_> rightTuple, |
136 | | - IndexedSet<OutTuple_> referenceOutTupleSet, IndexedSet<OutTuple_> outTupleSet, |
137 | | - int outputStoreIndexOutSet) { |
138 | | - if (!leftTuple.state.isActive()) { |
139 | | - // Assume the following scenario: |
140 | | - // - The join is of two entities of the same type, both filtering out unassigned. |
141 | | - // - One entity became unassigned, so the outTuple is getting retracted. |
142 | | - // - The other entity is still assigned and is being updated. |
143 | | - // |
144 | | - // This means the filter would be called with (unassignedEntity, assignedEntity), |
145 | | - // which breaks the expectation that the filter is only called on two assigned entities |
146 | | - // and requires adding null checks to the filter for something that should intuitively be impossible. |
147 | | - // We avoid this situation as it is clear that the outTuple must be retracted anyway, |
148 | | - // and therefore any further updates to it are pointless. |
149 | | - // |
150 | | - // It is possible that the same problem would exist coming from the other side as well, |
151 | | - // and therefore the right tuple would have to be checked for active state as well. |
152 | | - // However, no such issue could have been reproduced; when in doubt, leave it out. |
153 | | - return; |
154 | | - } |
155 | | - var outTuple = findOutTuple(outTupleSet, referenceOutTupleSet, outputStoreIndexOutSet); |
156 | | - if (testFiltering(leftTuple, rightTuple)) { |
157 | | - if (outTuple == null) { |
158 | | - insertOutTuple(leftTuple, rightTuple); |
159 | | - } else { |
160 | | - updateOutTupleLeft(outTuple, leftTuple); |
161 | | - } |
162 | | - } else { |
163 | | - if (outTuple != null) { |
164 | | - retractOutTuple(outTuple); |
165 | | - } |
166 | | - } |
167 | | - } |
168 | | - |
169 | | - private static <Tuple_ extends AbstractTuple> @Nullable Tuple_ findOutTuple(IndexedSet<Tuple_> outTupleSet, |
170 | | - IndexedSet<Tuple_> referenceOutTupleSet, int outputStoreIndexOutSet) { |
171 | | - // Hack: the outTuple has no left/right input tuple reference, use the left/right outSet reference instead. |
172 | | - return outTupleSet.findFirst(outTuple -> referenceOutTupleSet == outTuple.getStore(outputStoreIndexOutSet)); |
173 | | - } |
174 | | - |
175 | 187 | protected final void retractOutTuple(OutTuple_ outTuple) { |
176 | 188 | IndexedSet<OutTuple_> outSetLeft = outTuple.removeStore(outputStoreIndexLeftOutSet); |
177 | 189 | outSetLeft.remove(outTuple); |
|
0 commit comments