|
40 | 40 | import com.apple.foundationdb.record.query.plan.cascades.expressions.ExplodeExpression;
|
41 | 41 | import com.apple.foundationdb.record.query.plan.cascades.expressions.GroupByExpression;
|
42 | 42 | import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalSortExpression;
|
| 43 | +import com.apple.foundationdb.record.query.plan.cascades.matching.structure.BindingMatcher; |
43 | 44 | import com.apple.foundationdb.record.query.plan.cascades.matching.structure.ListMatcher;
|
44 | 45 | import com.apple.foundationdb.record.query.plan.cascades.matching.structure.RecordQueryPlanMatchers;
|
| 46 | +import com.apple.foundationdb.record.query.plan.cascades.matching.structure.ValueMatchers; |
45 | 47 | import com.apple.foundationdb.record.query.plan.cascades.predicates.QueryPredicate;
|
46 | 48 | import com.apple.foundationdb.record.query.plan.cascades.typing.Type;
|
47 | 49 | import com.apple.foundationdb.record.query.plan.cascades.values.ConstantObjectValue;
|
|
57 | 59 | import com.apple.test.Tags;
|
58 | 60 | import com.google.common.base.Predicates;
|
59 | 61 | import com.google.common.collect.ImmutableList;
|
| 62 | +import com.google.common.collect.ImmutableMap; |
60 | 63 | import com.google.protobuf.Descriptors;
|
61 | 64 | import com.google.protobuf.Message;
|
62 | 65 | import org.hamcrest.Matcher;
|
|
77 | 80 | import java.util.Map;
|
78 | 81 | import java.util.Optional;
|
79 | 82 | import java.util.Set;
|
| 83 | +import java.util.concurrent.atomic.AtomicLong; |
80 | 84 | import java.util.function.Function;
|
81 | 85 | import java.util.function.Predicate;
|
82 | 86 | import java.util.stream.Collectors;
|
|
94 | 98 | import static org.hamcrest.Matchers.contains;
|
95 | 99 | import static org.hamcrest.Matchers.containsInAnyOrder;
|
96 | 100 | import static org.hamcrest.Matchers.hasSize;
|
| 101 | +import static org.hamcrest.Matchers.lessThanOrEqualTo; |
97 | 102 | import static org.junit.jupiter.api.Assertions.assertEquals;
|
98 | 103 | import static org.junit.jupiter.api.Assertions.assertTrue;
|
99 | 104 |
|
@@ -456,7 +461,9 @@ void selectMaxWithInOrderByMax(InComparisonCase inComparisonCase) throws Excepti
|
456 | 461 | .where(RecordQueryPlanMatchers.scanComparisons(ScanComparisons.equalities(ListMatcher.exactly(ScanComparisons.anyValueComparison()))))
|
457 | 462 | .and(RecordQueryPlanMatchers.isReverse())
|
458 | 463 | )
|
459 |
| - )); |
| 464 | + ).where(RecordQueryPlanMatchers.comparisonKeyValues(ListMatcher.exactly( |
| 465 | + ValueMatchers.fieldValueWithFieldNames("m"), ValueMatchers.fieldValueWithFieldNames("num_value_2"), ValueMatchers.fieldValueWithFieldNames("num_value_3_indexed") |
| 466 | + )))); |
460 | 467 |
|
461 | 468 | assertEquals(inComparisonCase.getContinuationPlanHash(), plan.planHash(PlanHashable.CURRENT_FOR_CONTINUATION));
|
462 | 469 |
|
@@ -537,7 +544,9 @@ void testMaxWithInAndDupes(InComparisonCase inComparisonCase) throws Exception {
|
537 | 544 | .where(RecordQueryPlanMatchers.scanComparisons(ScanComparisons.equalities(ListMatcher.exactly(ScanComparisons.anyValueComparison()))))
|
538 | 545 | .and(RecordQueryPlanMatchers.isReverse())
|
539 | 546 | )
|
540 |
| - )); |
| 547 | + ).where(RecordQueryPlanMatchers.comparisonKeyValues(ListMatcher.exactly( |
| 548 | + ValueMatchers.fieldValueWithFieldNames("m"), ValueMatchers.fieldValueWithFieldNames("str_value_indexed"), ValueMatchers.fieldValueWithFieldNames("num_value_3_indexed") |
| 549 | + )))); |
541 | 550 |
|
542 | 551 | assertEquals(inComparisonCase.getContinuationPlanHash(), plan.planHash(PlanHashable.CURRENT_FOR_CONTINUATION));
|
543 | 552 |
|
@@ -690,7 +699,9 @@ void testSortedMaxWithInOnRepeater(InComparisonCase inComparisonCase) {
|
690 | 699 | RecordQueryPlanMatchers.aggregateIndexPlan()
|
691 | 700 | .where(RecordQueryPlanMatchers.isReverse())
|
692 | 701 | )
|
693 |
| - )); |
| 702 | + ).where(RecordQueryPlanMatchers.comparisonKeyValues(ListMatcher.exactly( |
| 703 | + ValueMatchers.fieldValueWithFieldNames("m"), ValueMatchers.fieldValueWithFieldNames("x"), ValueMatchers.fieldValueWithFieldNames("num_value_2")) |
| 704 | + ))); |
694 | 705 |
|
695 | 706 | assertEquals(inComparisonCase.getContinuationPlanHash(), plan.planHash(PlanHashable.CURRENT_FOR_CONTINUATION));
|
696 | 707 |
|
@@ -734,6 +745,154 @@ void testSortedMaxWithInOnRepeater(InComparisonCase inComparisonCase) {
|
734 | 745 | }
|
735 | 746 | }
|
736 | 747 |
|
| 748 | + @Nonnull |
| 749 | + static Stream<InComparisonCase> testMaxUniqueByStr2And3WithDifferentOrderingKeys() { |
| 750 | + ConstantObjectValue constant = ConstantObjectValue.of(Quantifier.uniqueID(), "0", new Type.Array(false, Type.primitiveType(Type.TypeCode.STRING, false))); |
| 751 | + List<String> literalStrList = ImmutableList.of("even", "odd", "empty", "other1", "other2"); |
| 752 | + return Stream.of( |
| 753 | + new InComparisonCase("byParameter", new Comparisons.ParameterComparison(Comparisons.Type.IN, "strValueList"), strValueList -> Bindings.newBuilder().set("strValueList", strValueList).build(), 755361732, -85959138), |
| 754 | + new InComparisonCase("byLiteral", new Comparisons.ListComparison(Comparisons.Type.IN, literalStrList), strValueList -> { |
| 755 | + Assumptions.assumeTrue(strValueList.equals(literalStrList)); |
| 756 | + return Bindings.EMPTY_BINDINGS; |
| 757 | + }, 381518259, -459802611), |
| 758 | + new InComparisonCase("byConstantObjectValue", new Comparisons.ValueComparison(Comparisons.Type.IN, constant), strValueList -> constantBindings(constant, strValueList), -603175313, -1444496183) |
| 759 | + ); |
| 760 | + } |
| 761 | + |
| 762 | + @DualPlannerTest(planner = DualPlannerTest.Planner.CASCADES) |
| 763 | + @ParameterizedTest(name = "testMaxUniqueByStr2And3WithDifferentOrderingKeys[inComparisonCase={0}]") |
| 764 | + @MethodSource |
| 765 | + void testMaxUniqueByStr2And3WithDifferentOrderingKeys(InComparisonCase inComparisonCase) throws Exception { |
| 766 | + Assumptions.assumeTrue(useCascadesPlanner); |
| 767 | + final RecordMetaDataHook hook = metaData -> { |
| 768 | + metaData.addIndex(metaData.getRecordType("MySimpleRecord"), maxUniqueByStrValueOrderBy2And3()); |
| 769 | + metaData.removeIndex("MySimpleRecord$num_value_unique"); // get rid of unique index as we don't want uniqueness constraint |
| 770 | + }; |
| 771 | + complexQuerySetup(hook); |
| 772 | + |
| 773 | + try (FDBRecordContext context = openContext()) { |
| 774 | + openSimpleRecordStore(context, hook); |
| 775 | + |
| 776 | + // Add in a few more records. These will serve to create duplicates with the records included in the |
| 777 | + // complex query setup hook |
| 778 | + final Map<Tuple, Integer> expected = expectedMaxUniquesByStrValueNumValue2NumValue3(); |
| 779 | + final AtomicLong recNoCounter = new AtomicLong(100_000L); |
| 780 | + saveOtherMax("other1", 980, 0, 0, recNoCounter, expected); |
| 781 | + saveOtherMax("other2", 980, 1, 0, recNoCounter, expected); |
| 782 | + saveOtherMax("other1", 992, 2, 0, recNoCounter, expected); |
| 783 | + saveOtherMax("other2", 992, 2, 1, recNoCounter, expected); |
| 784 | + saveOtherMax("other1", 972, 1, 3, recNoCounter, expected); |
| 785 | + saveOtherMax("other2", 972, 2, 3, recNoCounter, expected); |
| 786 | + |
| 787 | + final Object[] expectedArr = expected.entrySet().stream() |
| 788 | + .map(entry -> { |
| 789 | + Tuple key = entry.getKey(); |
| 790 | + return ImmutableMap.<String, Object>of( |
| 791 | + "str_value_indexed", key.getString(0), |
| 792 | + "num_value_2", (int) key.getLong(1), |
| 793 | + "num_value_3_indexed", (int) key.getLong(2), |
| 794 | + "m", entry.getValue()); |
| 795 | + }) |
| 796 | + .toArray(); |
| 797 | + |
| 798 | + planner.setConfiguration(planner.getConfiguration().asBuilder().setAttemptFailedInJoinAsUnionMaxSize(10).build()); |
| 799 | + |
| 800 | + // Issue a query like: |
| 801 | + // SELECT str_value_indexed, max(num_value_unique) as m, num_value_2, num_value_3_indexed |
| 802 | + // FROM MySimpleRecord |
| 803 | + // GROUP BY str_value_indexed, num_value_2_indexed, num_value_3 |
| 804 | + // HAVING str_value_indexed IN $strValueList |
| 805 | + // ORDER BY max(num_value_unique), num_value_2_indexed, num_value_3 |
| 806 | + // We will issue this with different ordering keys |
| 807 | + |
| 808 | + final List<String> totalOrderingKey = ImmutableList.of("m", "num_value_2", "num_value_3_indexed"); |
| 809 | + boolean checkedPlanHash = false; |
| 810 | + for (int i = 0; i <= totalOrderingKey.size(); i++) { |
| 811 | + final List<String> orderingKey = totalOrderingKey.subList(0, i); |
| 812 | + |
| 813 | + final RecordQueryPlan plan = planGraph(() -> { |
| 814 | + final var base = fullTypeScan(recordStore.getRecordMetaData(), "MySimpleRecord"); |
| 815 | + final var selectWhere = selectWhereQun(base, null); |
| 816 | + final var groupedByQun = maxByGroup(selectWhere, "num_value_unique", ImmutableList.of("str_value_indexed", "num_value_2", "num_value_3_indexed")); |
| 817 | + |
| 818 | + final var strValueReference = FieldValue.ofOrdinalNumberAndFuseIfPossible(FieldValue.ofOrdinalNumber(groupedByQun.getFlowedObjectValue(), 0), 0); |
| 819 | + final var qun = selectHaving(groupedByQun, |
| 820 | + strValueReference.withComparison(inComparisonCase.getComparison()), |
| 821 | + ImmutableList.of("str_value_indexed", "m", "num_value_2", "num_value_3_indexed")); |
| 822 | + final AliasMap aliasMap = AliasMap.ofAliases(qun.getAlias(), Quantifier.current()); |
| 823 | + final List<Value> sortValues = orderingKey.stream() |
| 824 | + .map(fieldName -> FieldValue.ofFieldName(qun.getFlowedObjectValue(), fieldName).rebase(aliasMap)) |
| 825 | + .collect(Collectors.toList()); |
| 826 | + return Reference.initialOf(sortExpression(sortValues, true, qun)); |
| 827 | + }); |
| 828 | + |
| 829 | + var aggregatePlanMatcher = RecordQueryPlanMatchers.aggregateIndexPlan() |
| 830 | + .where(RecordQueryPlanMatchers.scanComparisons(ScanComparisons.equalities(ListMatcher.exactly(ScanComparisons.anyValueComparison())))); |
| 831 | + if (orderingKey.isEmpty()) { |
| 832 | + // If we don't have an ordering constraint, use an IN-join. |
| 833 | + assertMatchesExactly(plan, |
| 834 | + RecordQueryPlanMatchers.inJoinPlan( |
| 835 | + RecordQueryPlanMatchers.mapPlan(aggregatePlanMatcher))); |
| 836 | + } else { |
| 837 | + // If we do have an ordering constraint, we need to use an IN-union. |
| 838 | + // The ordering key should be: |
| 839 | + // 1. Fields in the requested order |
| 840 | + // 2. The str_value_indexed field (meaning that we'll consume all values from a leg of the union for a fixed value of the ordering key) |
| 841 | + // 3. Any remaining elements in the underlying order |
| 842 | + final List<BindingMatcher<FieldValue>> comparisonKeyFieldMatchers = Stream.concat( |
| 843 | + orderingKey.stream(), |
| 844 | + Stream.concat(Stream.of("str_value_indexed"), totalOrderingKey.subList(i, totalOrderingKey.size()).stream()) |
| 845 | + ).map(ValueMatchers::fieldValueWithFieldNames).collect(Collectors.toList()); |
| 846 | + assertMatchesExactly(plan, |
| 847 | + RecordQueryPlanMatchers.inUnionOnValuesPlan( |
| 848 | + RecordQueryPlanMatchers.mapPlan( |
| 849 | + aggregatePlanMatcher.and(RecordQueryPlanMatchers.isReverse()))) |
| 850 | + .where(RecordQueryPlanMatchers.comparisonKeyValues(ListMatcher.exactly(comparisonKeyFieldMatchers))) |
| 851 | + ); |
| 852 | + } |
| 853 | + |
| 854 | + // When we have a total ordering, compare the plan hashes. We only check this one plan because we only encode |
| 855 | + // one plan hash overall, and we've hard-coded the one with the largest comparison key |
| 856 | + if (orderingKey.size() == totalOrderingKey.size()) { |
| 857 | + assertEquals(inComparisonCase.getContinuationPlanHash(), plan.planHash(PlanHashable.CURRENT_FOR_CONTINUATION)); |
| 858 | + assertEquals(inComparisonCase.getLegacyPlanHash(), plan.planHash(PlanHashable.CURRENT_LEGACY)); |
| 859 | + checkedPlanHash = true; |
| 860 | + } |
| 861 | + |
| 862 | + // Validate contents |
| 863 | + final List<Map<String, Object>> queriedList = queryAsMaps(plan, inComparisonCase.getBindings(ImmutableList.of("even", "odd", "empty", "other1", "other2"))); |
| 864 | + assertThat(queriedList, containsInAnyOrder(expectedArr)); |
| 865 | + |
| 866 | + // Validate ordering |
| 867 | + Tuple lastSeen = null; |
| 868 | + for (Map<String, Object> queried : queriedList) { |
| 869 | + Tuple comparisonKey = Tuple.fromItems(orderingKey.stream().map(queried::get).collect(Collectors.toList())); |
| 870 | + if (lastSeen != null) { |
| 871 | + assertThat(comparisonKey, lessThanOrEqualTo(lastSeen)); |
| 872 | + } |
| 873 | + lastSeen = comparisonKey; |
| 874 | + } |
| 875 | + } |
| 876 | + |
| 877 | + // Validate that we actually checked the plan hash. This also double checks that we try the full comparison |
| 878 | + // key case, which is also the case that exposes the bug alluded to by: |
| 879 | + // https://github.com/FoundationDB/fdb-record-layer/issues/3331 |
| 880 | + assertTrue(checkedPlanHash, "should have checked plan hashes during test"); |
| 881 | + } |
| 882 | + } |
| 883 | + |
| 884 | + private void saveOtherMax(@Nonnull String strValue, int numValueUnique, int numValue2, int numValue3, @Nonnull AtomicLong recNoCounter, @Nonnull Map<Tuple, Integer> collector) { |
| 885 | + recordStore.saveRecord(TestRecords1Proto.MySimpleRecord.newBuilder() |
| 886 | + .setRecNo(recNoCounter.getAndIncrement()) |
| 887 | + .setStrValueIndexed(strValue) |
| 888 | + .setNumValueUnique(numValueUnique) |
| 889 | + .setNumValue2(numValue2) |
| 890 | + .setNumValue3Indexed(numValue3) |
| 891 | + .build()); |
| 892 | + collector.compute(Tuple.from(strValue, numValue2, numValue3), |
| 893 | + (key, oldMax) -> oldMax == null || oldMax < numValueUnique ? numValueUnique : oldMax); |
| 894 | + } |
| 895 | + |
737 | 896 | @DualPlannerTest(planner = DualPlannerTest.Planner.CASCADES)
|
738 | 897 | void selectMaxGroupByWithPredicateOnMax() throws Exception {
|
739 | 898 | final RecordMetaDataHook hook = metaData -> metaData.addIndex(metaData.getRecordType("MySimpleRecord"), maxUniqueBy2And3());
|
@@ -960,6 +1119,19 @@ private void setUpWithRepeaters(@Nullable RecordMetaDataHook hook) {
|
960 | 1119 | }
|
961 | 1120 | }
|
962 | 1121 |
|
| 1122 | + @Nonnull |
| 1123 | + private static Map<Tuple, Integer> expectedMaxUniquesByStrValueNumValue2NumValue3() { |
| 1124 | + Map<Tuple, Integer> expected = new HashMap<>(); |
| 1125 | + for (String strValue : List.of("even", "odd")) { |
| 1126 | + for (int numValue2 = 0; numValue2 < 3; numValue2++) { |
| 1127 | + final int nv2 = numValue2; |
| 1128 | + Map<Integer, Integer> maxBy3 = expectedMaxUniquesByNumValue3(x -> x == nv2, strValue::equals); |
| 1129 | + maxBy3.forEach((nv3, maxUnique) -> expected.put(Tuple.from(strValue, nv2, nv3), maxUnique)); |
| 1130 | + } |
| 1131 | + } |
| 1132 | + return expected; |
| 1133 | + } |
| 1134 | + |
963 | 1135 | @Nonnull
|
964 | 1136 | private static Map<Integer, Integer> expectedMaxUniquesByNumValue3(Predicate<Integer> numValue2Filter) {
|
965 | 1137 | return expectedMaxUniquesByNumValue3(numValue2Filter, Predicates.alwaysTrue());
|
|
0 commit comments