2020import static org .junit .Assert .assertEquals ;
2121import static org .junit .Assert .assertFalse ;
2222import static org .junit .Assert .assertTrue ;
23+ import static org .mockito .ArgumentMatchers .any ;
24+ import static org .mockito .ArgumentMatchers .eq ;
25+ import static org .mockito .BDDMockito .given ;
26+ import static org .mockito .Mockito .mock ;
2327
2428import java .text .ParseException ;
29+ import java .util .List ;
2530
2631import org .apache .jackrabbit .oak .namepath .NamePathMapper ;
2732import org .apache .jackrabbit .oak .query .ast .NodeTypeInfoProvider ;
2833import org .apache .jackrabbit .oak .query .stats .QueryStatsData ;
2934import org .apache .jackrabbit .oak .query .xpath .XPathToSQL2Converter ;
35+ import org .apache .jackrabbit .oak .spi .query .QueryIndex ;
36+ import org .apache .jackrabbit .oak .spi .query .QueryIndexProvider ;
37+ import org .apache .jackrabbit .oak .spi .state .NodeState ;
38+ import org .jetbrains .annotations .NotNull ;
3039import org .junit .Ignore ;
3140import org .junit .Test ;
3241
@@ -40,14 +49,12 @@ public class SQL2ParserTest {
4049 private static final SQL2Parser p = createTestSQL2Parser ();
4150
4251 public static SQL2Parser createTestSQL2Parser () {
43- return createTestSQL2Parser (NamePathMapper .DEFAULT , nodeTypes , new QueryEngineSettings () );
52+ return createTestSQL2Parser (NamePathMapper .DEFAULT , nodeTypes );
4453 }
4554
46- public static SQL2Parser createTestSQL2Parser (NamePathMapper mappings , NodeTypeInfoProvider nodeTypes2 ,
47- QueryEngineSettings qeSettings ) {
55+ public static SQL2Parser createTestSQL2Parser (NamePathMapper mappings , NodeTypeInfoProvider nodeTypes2 ) {
4856 QueryStatsData data = new QueryStatsData ("" , "" );
49- return new SQL2Parser (mappings , nodeTypes2 , new QueryEngineSettings (),
50- data .new QueryExecutionStats ());
57+ return new SQL2Parser (mappings , nodeTypes2 , new QueryEngineSettings (), data .new QueryExecutionStats ());
5158 }
5259
5360
@@ -87,6 +94,60 @@ public void testUnwrappedOr() throws ParseException {
8794 assertTrue (q .contains (token ));
8895 }
8996
97+ /*
98+ * When a query with LIMIT option, it should still select the index with the least entries
99+ * as those might require being traversed during post-filtering.
100+ *
101+ * See OAK-12057
102+ */
103+ @ Test
104+ public void testPlanningWithLimit () throws ParseException {
105+ // Given
106+ var query = "SELECT * \n " +
107+ "FROM [nt:base] AS s \n " +
108+ "WHERE ISDESCENDANTNODE(s, '/content') AND s.[j:c]='/conf/wknd'\n " +
109+ "OPTION (LIMIT 2)" ;
110+
111+ // - the first available option
112+ var indexA = mock (QueryIndex .AdvancedQueryIndex .class );
113+ var planA = mock (QueryIndex .IndexPlan .class );
114+ given (indexA .getPlans (any (), any (), any ())).willReturn (List .of (planA ));
115+ given (planA .getPlanName ()).willReturn ("planA" );
116+ given (planA .getEstimatedEntryCount ()).willReturn (10000L ); // more entries
117+ given (planA .getCostPerEntry ()).willReturn (1.0 );
118+ given (planA .getCostPerExecution ()).willReturn (100.0 );
119+
120+ // - the better option
121+ var indexB = mock (QueryIndex .AdvancedQueryIndex .class );
122+ var planB = mock (QueryIndex .IndexPlan .class );
123+ given (indexB .getPlans (any (), any (), any ())).willReturn (List .of (planB ));
124+ given (planB .getPlanName ()).willReturn ("planB" );
125+ given (planB .getEstimatedEntryCount ()).willReturn (100L ); // less entries
126+ given (planB .getCostPerEntry ()).willReturn (1.0 );
127+ given (planB .getCostPerExecution ()).willReturn (100.0 );
128+ given (indexB .getPlanDescription (eq (planB ), any ())).willReturn ("planB" );
129+
130+ var indexProvider = new QueryIndexProvider () {
131+ @ Override
132+ public @ NotNull List <? extends QueryIndex > getQueryIndexes (NodeState nodeState ) {
133+ return List .of (indexA , indexB );
134+ }
135+ };
136+
137+ var context = mock (ExecutionContext .class );
138+ given (context .getIndexProvider ()).willReturn (indexProvider );
139+
140+ // When
141+ var parsedQuery = p .parse (query ,false );
142+ parsedQuery .init ();
143+ parsedQuery .setExecutionContext (context );
144+ parsedQuery .setTraversalEnabled (false );
145+ parsedQuery .prepare ();
146+
147+ // Then
148+ assertEquals ("[nt:base] as [s] /* planB */" , parsedQuery .getPlan ());
149+ }
150+
90151 @ Test
91152 public void testCoalesce () throws ParseException {
92153 p .parse ("SELECT * FROM [nt:base] WHERE COALESCE([j:c/m/d:t], [j:c/j:t])='a'" );
@@ -188,4 +249,5 @@ public void orderingWithUnionOfNodetype() throws Exception {
188249 xpath = "//(element(*, type1) | element(*, type2))[@a='b' or @c='d'] order by @foo" ;
189250 assertTrue ("Converted xpath " + xpath + "doesn't end with 'order by [foo]'" , c .convert (xpath ).endsWith ("order by [foo]" ));
190251 }
252+
191253}
0 commit comments