Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0e606d2

Browse files
committedMay 16, 2025
Add AOT support for dynamic projections, streaming/scroll queries and Meta annotation.
Closes: #4970
1 parent 95e0ec3 commit 0e606d2

File tree

9 files changed

+208
-32
lines changed

9 files changed

+208
-32
lines changed
 

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.bson.Document;
2424
import org.jspecify.annotations.NullUnmarked;
2525
import org.jspecify.annotations.Nullable;
26+
2627
import org.springframework.core.annotation.MergedAnnotation;
2728
import org.springframework.data.domain.SliceImpl;
2829
import org.springframework.data.domain.Sort.Order;
@@ -40,6 +41,7 @@
4041
import org.springframework.data.mongodb.core.query.BasicUpdate;
4142
import org.springframework.data.mongodb.core.query.Collation;
4243
import org.springframework.data.mongodb.repository.Hint;
44+
import org.springframework.data.mongodb.repository.Meta;
4345
import org.springframework.data.mongodb.repository.ReadPreference;
4446
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecution;
4547
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution;
@@ -256,15 +258,13 @@ CodeBlock build() {
256258
updateReference);
257259
} else if (ClassUtils.isAssignable(Long.class, returnType)) {
258260
builder.addStatement("return $L.matching($L).apply($L).all().getModifiedCount()",
259-
context.localVariable("updater"), queryVariableName,
260-
updateReference);
261+
context.localVariable("updater"), queryVariableName, updateReference);
261262
} else {
262263
builder.addStatement("$T $L = $L.matching($L).apply($L).all().getModifiedCount()", Long.class,
263-
context.localVariable("modifiedCount"), context.localVariable("updater"),
264-
queryVariableName, updateReference);
264+
context.localVariable("modifiedCount"), context.localVariable("updater"), queryVariableName,
265+
updateReference);
265266
builder.addStatement("return $T.convertNumberToTargetClass($L, $T.class)", NumberUtils.class,
266-
context.localVariable("modifiedCount"),
267-
returnType);
267+
context.localVariable("modifiedCount"), returnType);
268268
}
269269

270270
return builder.build();
@@ -319,11 +319,9 @@ CodeBlock build() {
319319
Class<?> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType());
320320

321321
builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class,
322-
context.localVariable("results"), mongoOpsRef,
323-
aggregationVariableName, outputType);
322+
context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
324323
if (!queryMethod.isCollectionQuery()) {
325-
builder.addStatement(
326-
"return $T.<$T>firstElement(convertSimpleRawResults($T.class, $L.getMappedResults()))",
324+
builder.addStatement("return $T.<$T>firstElement(convertSimpleRawResults($T.class, $L.getMappedResults()))",
327325
CollectionUtils.class, returnType, returnType, context.localVariable("results"));
328326
} else {
329327
builder.addStatement("return convertSimpleRawResults($T.class, $L.getMappedResults())", returnType,
@@ -332,8 +330,7 @@ CodeBlock build() {
332330
} else {
333331
if (queryMethod.isSliceQuery()) {
334332
builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class,
335-
context.localVariable("results"), mongoOpsRef,
336-
aggregationVariableName, outputType);
333+
context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
337334
builder.addStatement("boolean $L = $L.getMappedResults().size() > $L.getPageSize()",
338335
context.localVariable("hasNext"), context.localVariable("results"), context.getPageableParameterName());
339336
builder.addStatement(
@@ -378,12 +375,16 @@ CodeBlock build() {
378375

379376
boolean isProjecting = context.getReturnedType().isProjecting();
380377
Class<?> domainType = context.getRepositoryInformation().getDomainType();
381-
Object actualReturnType = isProjecting ? context.getActualReturnType().getType()
378+
Object actualReturnType = queryMethod.getParameters().hasDynamicProjection() || isProjecting
379+
? TypeName.get(context.getActualReturnType().getType())
382380
: domainType;
383381

384382
builder.add("\n");
385383

386-
if (isProjecting) {
384+
if (queryMethod.getParameters().hasDynamicProjection()) {
385+
builder.addStatement("$T<$T> $L = $L.query($T.class).as($L)", FindWithQuery.class, actualReturnType,
386+
context.localVariable("finder"), mongoOpsRef, domainType, context.getDynamicProjectionParameterName());
387+
} else if (isProjecting) {
387388
builder.addStatement("$T<$T> $L = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType,
388389
context.localVariable("finder"), mongoOpsRef, domainType, actualReturnType);
389390
} else {
@@ -400,6 +401,8 @@ CodeBlock build() {
400401
terminatingMethod = "count()";
401402
} else if (query.isExists()) {
402403
terminatingMethod = "exists()";
404+
} else if (queryMethod.isStreamQuery()) {
405+
terminatingMethod = "stream()";
403406
} else {
404407
terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" : "oneValue()";
405408
}
@@ -410,6 +413,12 @@ CodeBlock build() {
410413
} else if (queryMethod.isSliceQuery()) {
411414
builder.addStatement("return new $T($L, $L).execute($L)", SlicedExecution.class,
412415
context.localVariable("finder"), context.getPageableParameterName(), query.name());
416+
} else if (queryMethod.isScrollQuery()) {
417+
418+
String scrollPositionParameterName = context.getScrollPositionParameterName();
419+
420+
builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(),
421+
scrollPositionParameterName);
413422
} else {
414423
builder.addStatement("return $L.matching($L).$L", context.localVariable("finder"), query.name(),
415424
terminatingMethod);
@@ -544,8 +553,7 @@ private CodeBlock aggregationOptions(String aggregationVariableName) {
544553

545554
Builder optionsBuilder = CodeBlock.builder();
546555
optionsBuilder.add("$T $L = $T.builder()\n", AggregationOptions.class,
547-
context.localVariable("aggregationOptions"),
548-
AggregationOptions.class);
556+
context.localVariable("aggregationOptions"), AggregationOptions.class);
549557
optionsBuilder.indent();
550558
for (CodeBlock optionBlock : options) {
551559
optionsBuilder.add(optionBlock);
@@ -709,7 +717,27 @@ CodeBlock build() {
709717
com.mongodb.ReadPreference.class, readPreference);
710718
}
711719

712-
// TODO: Meta annotation
720+
MergedAnnotation<Meta> metaAnnotation = context.getAnnotation(Meta.class);
721+
722+
if (metaAnnotation.isPresent()) {
723+
724+
long maxExecutionTimeMs = metaAnnotation.getLong("maxExecutionTimeMs");
725+
if (maxExecutionTimeMs != -1) {
726+
builder.addStatement("$L.maxTimeMsec($L)", queryVariableName, maxExecutionTimeMs);
727+
}
728+
729+
int cursorBatchSize = metaAnnotation.getInt("cursorBatchSize");
730+
if (cursorBatchSize != 0) {
731+
builder.addStatement("$L.cursorBatchSize($L)", queryVariableName, cursorBatchSize);
732+
}
733+
734+
String comment = metaAnnotation.getString("comment");
735+
if (StringUtils.hasText("comment")) {
736+
builder.addStatement("$L.comment($S)", queryVariableName, comment);
737+
}
738+
}
739+
740+
// TODO: Meta annotation: Disk usage
713741

714742
return builder.build();
715743
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
* MongoDB specific {@link RepositoryContributor}.
4949
*
5050
* @author Christoph Strobl
51+
* @author Mark Paluch
5152
* @since 5.0
5253
*/
5354
public class MongoRepositoryContributor extends RepositoryContributor {
@@ -159,8 +160,7 @@ private QueryInteraction createStringQuery(RepositoryInformation repositoryInfor
159160

160161
private static boolean backoff(MongoQueryMethod method) {
161162

162-
boolean skip = method.isGeoNearQuery() || method.isScrollQuery() || method.isStreamQuery()
163-
|| method.isSearchQuery();
163+
boolean skip = method.isGeoNearQuery() || method.isSearchQuery();
164164

165165
if (skip && logger.isDebugEnabled()) {
166166
logger.debug("Skipping AOT generation for [%s]. Method is either geo-near, streaming, search or scrolling query"
@@ -225,8 +225,7 @@ private static MethodContributor<MongoQueryMethod> aggregationUpdateMethodContri
225225
.usingAggregationVariableName(updateVariableName).pipelineOnly(true).build());
226226

227227
builder.addStatement("$T $L = $T.from($L.getOperations())", AggregationUpdate.class,
228-
context.localVariable("aggregationUpdate"),
229-
AggregationUpdate.class, updateVariableName);
228+
context.localVariable("aggregationUpdate"), AggregationUpdate.class, updateVariableName);
230229

231230
builder.add(updateExecutionBlockBuilder(context, queryMethod).withFilter(filterVariableName)
232231
.referencingUpdate(context.localVariable("aggregationUpdate")).build());

‎spring-data-mongodb/src/test/java/example/aot/UserRepository.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@
2222
import java.util.Objects;
2323
import java.util.Optional;
2424
import java.util.Set;
25+
import java.util.stream.Stream;
2526

2627
import org.springframework.data.annotation.Id;
2728
import org.springframework.data.domain.Limit;
2829
import org.springframework.data.domain.Page;
2930
import org.springframework.data.domain.Pageable;
31+
import org.springframework.data.domain.ScrollPosition;
3032
import org.springframework.data.domain.Slice;
3133
import org.springframework.data.domain.Sort;
34+
import org.springframework.data.domain.Window;
3235
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
3336
import org.springframework.data.mongodb.repository.Aggregation;
3437
import org.springframework.data.mongodb.repository.Hint;
@@ -94,8 +97,10 @@ public interface UserRepository extends CrudRepository<User, String> {
9497

9598
Slice<User> findSliceOfUserByLastnameStartingWith(String lastname, Pageable page);
9699

97-
// TODO: Streaming
98-
// TODO: Scrolling
100+
Stream<User> streamByLastnameStartingWith(String lastname, Sort sort, Limit limit);
101+
102+
Window<User> findTop2WindowByLastnameStartingWithOrderByUsername(String lastname, ScrollPosition scrollPosition);
103+
99104
// TODO: GeoQueries
100105
// TODO: TextSearch
101106

@@ -176,14 +181,14 @@ public interface UserRepository extends CrudRepository<User, String> {
176181
@ReadPreference("no-such-read-preference")
177182
User findWithReadPreferenceByUsername(String username);
178183

179-
// TODO: hints
180-
181184
/* Projecting Queries */
182185

183186
List<UserProjection> findUserProjectionByLastnameStartingWith(String lastname);
184187

185188
Page<UserProjection> findUserProjectionByLastnameStartingWith(String lastname, Pageable page);
186189

190+
<T> Page<T> findUserProjectionByLastnameStartingWith(String lastname, Pageable page, Class<T> projectionType);
191+
187192
/* Aggregations */
188193

189194
@Aggregation(pipeline = { //

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
* This configuration generates the AOT repository, compiles sources and configures a BeanFactory to contain the AOT
4141
* fragment. Additionally, the fragment is exposed through a {@code repositoryInterface} JDK proxy forwarding method
4242
* invocations to the backing AOT fragment. Note that {@code repositoryInterface} is not a repository proxy.
43-
*
43+
*
4444
* @author Christoph Strobl
4545
*/
4646
public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor {
@@ -62,7 +62,8 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
6262
new MongoRepositoryContributor(repositoryContext).contribute(generationContext);
6363

6464
AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder
65-
.genericBeanDefinition(repositoryInterface.getName() + "Impl__Aot") //
65+
.genericBeanDefinition(
66+
repositoryInterface.getPackageName() + "." + repositoryInterface.getSimpleName() + "Impl__Aot") //
6667
.addConstructorArgReference("mongoOperations") //
6768
.addConstructorArgValue(getCreationContext(repositoryContext)).getBeanDefinition();
6869

@@ -80,6 +81,8 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
8081
}).getBeanDefinition();
8182

8283
((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragmentFacade", fragmentFacade);
84+
85+
beanFactory.registerSingleton("generationContext", generationContext);
8386
}
8487

8588
private Object getFragmentFacadeProxy(Object fragment) {

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,15 @@
3434
import org.springframework.beans.factory.annotation.Autowired;
3535
import org.springframework.context.annotation.Bean;
3636
import org.springframework.context.annotation.Configuration;
37+
import org.springframework.data.domain.KeysetScrollPosition;
3738
import org.springframework.data.domain.Limit;
39+
import org.springframework.data.domain.OffsetScrollPosition;
3840
import org.springframework.data.domain.Page;
3941
import org.springframework.data.domain.PageRequest;
42+
import org.springframework.data.domain.ScrollPosition;
4043
import org.springframework.data.domain.Slice;
4144
import org.springframework.data.domain.Sort;
45+
import org.springframework.data.domain.Window;
4246
import org.springframework.data.mongodb.core.MongoOperations;
4347
import org.springframework.data.mongodb.core.MongoTemplate;
4448
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
@@ -271,6 +275,37 @@ void testDerivedFinderReturningSlice() {
271275
assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo");
272276
}
273277

278+
@Test
279+
void testDerivedQueryReturningStream() {
280+
281+
List<User> results = fragment.streamByLastnameStartingWith("S", Sort.by("username"), Limit.of(2)).toList();
282+
283+
assertThat(results).hasSize(2);
284+
assertThat(results).extracting(User::getUsername).containsExactly("han", "kylo");
285+
}
286+
287+
@Test
288+
void testDerivedQueryReturningWindowByOffset() {
289+
290+
Window<User> window1 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", ScrollPosition.offset());
291+
assertThat(window1).extracting(User::getUsername).containsExactly("han", "kylo");
292+
assertThat(window1.positionAt(1)).isInstanceOf(OffsetScrollPosition.class);
293+
294+
Window<User> window2 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", window1.positionAt(1));
295+
assertThat(window2).extracting(User::getUsername).containsExactly("luke", "vader");
296+
}
297+
298+
@Test
299+
void testDerivedQueryReturningWindowByKeyset() {
300+
301+
Window<User> window1 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", ScrollPosition.keyset());
302+
assertThat(window1).extracting(User::getUsername).containsExactly("han", "kylo");
303+
assertThat(window1.positionAt(1)).isInstanceOf(KeysetScrollPosition.class);
304+
305+
Window<User> window2 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", window1.positionAt(1));
306+
assertThat(window2).extracting(User::getUsername).containsExactly("luke", "vader");
307+
}
308+
274309
@Test
275310
void testAnnotatedFinderReturningSingleValueWithQuery() {
276311

@@ -439,6 +474,14 @@ void testDerivedFinderReturningPageOfProjections() {
439474
assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo");
440475
}
441476

477+
@Test
478+
void testDerivedFinderReturningPageOfDynamicProjections() {
479+
480+
Page<UserProjection> users = fragment.findUserProjectionByLastnameStartingWith("S",
481+
PageRequest.of(0, 2, Sort.by("username")), UserProjection.class);
482+
assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo");
483+
}
484+
442485
@Test
443486
void testUpdateWithDerivedQuery() {
444487

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.repository.aot;
17+
18+
import static org.mockito.Mockito.*;
19+
import static org.springframework.data.mongodb.test.util.Assertions.*;
20+
21+
import example.aot.User;
22+
import example.aot.UserRepository;
23+
24+
import java.io.IOException;
25+
import java.nio.charset.StandardCharsets;
26+
27+
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.api.extension.ExtendWith;
29+
30+
import org.springframework.aot.generate.GeneratedFiles;
31+
import org.springframework.aot.test.generate.TestGenerationContext;
32+
import org.springframework.beans.factory.annotation.Autowired;
33+
import org.springframework.context.annotation.Bean;
34+
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.core.io.InputStreamResource;
36+
import org.springframework.core.io.InputStreamSource;
37+
import org.springframework.data.mongodb.core.MongoOperations;
38+
import org.springframework.data.mongodb.repository.Meta;
39+
import org.springframework.data.mongodb.test.util.MongoClientExtension;
40+
import org.springframework.data.repository.CrudRepository;
41+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
42+
43+
/**
44+
* Unit tests for the {@link UserRepository} fragment sources via {@link MongoRepositoryContributor}.
45+
*
46+
* @author Mark Paluch
47+
*/
48+
@ExtendWith(MongoClientExtension.class)
49+
@SpringJUnitConfig(classes = MongoRepositoryContributorUnitTests.MongoRepositoryContributorConfiguration.class)
50+
class MongoRepositoryContributorUnitTests {
51+
52+
@Configuration
53+
static class MongoRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport {
54+
55+
public MongoRepositoryContributorConfiguration() {
56+
super(MetaUserRepository.class);
57+
}
58+
59+
@Bean
60+
MongoOperations mongoOperations() {
61+
return mock(MongoOperations.class);
62+
}
63+
64+
}
65+
66+
@Autowired TestGenerationContext generationContext;
67+
68+
@Test
69+
void shouldConsiderMetaAnnotation() throws IOException {
70+
71+
InputStreamSource aotFragment = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.SOURCE,
72+
MetaUserRepository.class.getPackageName().replace('.', '/') + "/MetaUserRepositoryImpl__Aot.java");
73+
74+
String content = new InputStreamResource(aotFragment).getContentAsString(StandardCharsets.UTF_8);
75+
76+
assertThat(content).contains("filterQuery.maxTimeMsec(555)");
77+
assertThat(content).contains("filterQuery.cursorBatchSize(1234)");
78+
assertThat(content).contains("filterQuery.comment(\"foo\")");
79+
}
80+
81+
interface MetaUserRepository extends CrudRepository<User, String> {
82+
83+
@Meta
84+
User findAllByLastname(String lastname);
85+
86+
@Meta(cursorBatchSize = 1234, comment = "foo", maxExecutionTimeMs = 555)
87+
User findWithMetaAllByLastname(String lastname);
88+
}
89+
90+
}

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ public Class<?> getReturnedDomainClass(Method method) {
6969
return metadata.getReturnedDomainClass(method);
7070
}
7171

72+
@Override
73+
public TypeInformation<?> getReturnedDomainTypeInformation(Method method) {
74+
return metadata.getReturnedDomainTypeInformation(method);
75+
}
76+
7277
@Override
7378
public CrudMethods getCrudMethods() {
7479
return metadata.getCrudMethods();

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.core.test.tools.ClassFile;
3131
import org.springframework.data.mongodb.core.mapping.Document;
3232
import org.springframework.data.repository.config.AotRepositoryContext;
33+
import org.springframework.data.repository.config.RepositoryConfigurationSource;
3334
import org.springframework.data.repository.core.RepositoryInformation;
3435
import org.springframework.data.repository.core.support.RepositoryComposition;
3536

@@ -70,6 +71,11 @@ public String getModuleName() {
7071
return "MongoDB";
7172
}
7273

74+
@Override
75+
public RepositoryConfigurationSource getConfigurationSource() {
76+
return null;
77+
}
78+
7379
@Override
7480
public Set<String> getBasePackages() {
7581
return Set.of("org.springframework.data.dummy.repository.aot");

‎src/main/antora/modules/ROOT/pages/mongodb/aot.adoc

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,23 +66,20 @@ These are typically all query methods that are not backed by an xref:repositorie
6666
* Query methods annotated with `@Query` (excluding those containing SpEL)
6767
* Methods annotated with `@Aggregation`
6868
* Methods using `@Update`
69-
* `@Hint` & `@ReadPreference` support
69+
* `@Hint`, `@Meta`, and `@ReadPreference` support
7070
* `Page`, `Slice`, and `Optional` return types
7171
* DTO Projections
7272

7373
**Limitations**
7474

75-
* `@Meta` annotations are not evaluated.
75+
* `@Meta.allowDiskUse` and `flags` are not evaluated.
7676
* Queries / Aggregations / Updates containing `SpEL` cannot be generated.
7777
* Limited `Collation` detection.
78-
* Reserved parameter names (must not be used in method signature) `finder`, `filterQuery`, `countQuery`, `deleteQuery`, `remover` `updateDefinition`, `aggregation`, `aggregationPipeline`, `aggregationUpdate`, `aggregationOptions`, `updater`, `results`, `fields`.
7978

8079
**Excluded methods**
8180

8281
* `CrudRepository` and other base interface methods
8382
* Querydsl and Query by Example methods
8483
* Methods whose implementation would be overly complex
8584
* Query Methods obtaining MQL from a file
86-
** Methods accepting `ScrollPosition` (e.g. `Keyset` pagination)
87-
** Dynamic projections
8885
** Geospatial Queries

0 commit comments

Comments
 (0)
Please sign in to comment.