Skip to content

Commit 4367386

Browse files
authored
Merge pull request #345 from eclipse-jnosql/optimize-count-queries
Optimized count query methods for MongoDB, Cassandra, ArangoDB, and Couchbase database drivers
2 parents f76d65a + ecd655c commit 4367386

File tree

18 files changed

+293
-57
lines changed

18 files changed

+293
-57
lines changed

CHANGELOG.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Version
88

99
== [Unreleased]
1010

11+
=== Added
12+
13+
- Add count method to MongoDB Document Manager for optimized count queries
14+
- Add count method to Cassandra Column Manager with consistency level support
15+
- Add count method to Cassandra Template with entity mapping
16+
- Add count method to ArangoDB Document Manager using AQL LENGTH function
17+
- Add count method to Couchbase Document Manager using N1QL COUNT queries
18+
1119
=== Changed
1220

1321
- Update OrientDB driver to 3.2.44
@@ -20,6 +28,9 @@ and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Version
2028
- Update Hazelcast to version 5.6.0
2129
- Update Redis driver to version 7.0.0
2230
- Update Oracle driver to version 5.4.18
31+
- Enhance QueryAQLConverter to support count query generation for ArangoDB
32+
- Enhance N1QLBuilder to support count query generation for Couchbase
33+
- Enhance QueryExecutor in Cassandra to support count operations
2334

2435
== [1.1.10] - 2025-08-19
2536

jnosql-arangodb/src/main/java/org/eclipse/jnosql/databases/arangodb/communication/DefaultArangoDBDocumentManager.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,14 @@ public long count(String documentCollection) {
148148
.map(Number::longValue).orElse(0L);
149149
}
150150

151+
@Override
152+
public long count(SelectQuery query) {
153+
requireNonNull(query, "query is required");
154+
checkCollection(query.name());
155+
AQLQueryResult aqlQuery = QueryAQLConverter.count(query);
156+
LOGGER.finest("Executing AQL: " + aqlQuery.query());
157+
return aql(aqlQuery.query(), aqlQuery.values(), Long.class).findFirst().orElse(0L);
158+
}
151159

152160
@Override
153161
public Stream<CommunicationEntity> aql(String query, Map<String, Object> params) throws NullPointerException {

jnosql-arangodb/src/main/java/org/eclipse/jnosql/databases/arangodb/communication/QueryAQLConverter.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,23 @@ public static AQLQueryResult select(SelectQuery query) throws NullPointerExcepti
7777

7878
}
7979

80+
public static AQLQueryResult count(SelectQuery query) throws NullPointerException {
81+
82+
AQLQueryResult q = convert(query.name(),
83+
query.condition().orElse(null),
84+
Collections.emptyList(),
85+
0L,
86+
0L,
87+
RETURN, false);
88+
89+
StringBuilder aql = new StringBuilder();
90+
91+
aql.append("RETURN LENGTH(")
92+
.append(q.query())
93+
.append(")");
94+
return new AQLQueryResult(aql.toString(), q.values());
95+
}
96+
8097

8198
private static AQLQueryResult convert(String documentCollection,
8299
CriteriaCondition documentCondition,

jnosql-arangodb/src/test/java/org/eclipse/jnosql/databases/arangodb/communication/ArangoDBDocumentManagerTest.java

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import static java.util.Collections.emptyMap;
4747
import static java.util.Collections.singletonMap;
4848
import static org.assertj.core.api.Assertions.assertThat;
49+
import static org.assertj.core.api.SoftAssertions.assertSoftly;
4950
import static org.eclipse.jnosql.communication.driver.IntegrationTest.MATCHES;
5051
import static org.eclipse.jnosql.communication.driver.IntegrationTest.NAMED;
5152
import static org.eclipse.jnosql.communication.semistructured.DeleteQuery.delete;
@@ -225,6 +226,22 @@ void shouldCount() {
225226
assertTrue(entityManager.count(COLLECTION_NAME) > 0);
226227
}
227228

229+
@Test
230+
void shouldCountWithSelectQuery() {
231+
CommunicationEntity entity = entityManager.insert(createDocumentListNotHavingId());
232+
Element key = entity.find(KEY_NAME).get();
233+
SelectQuery query = select().from("AppointmentBook").where(key.name()).eq(key.get()).build();
234+
235+
assertSoftly(softly -> {
236+
softly.assertThat(entityManager.count(query))
237+
.as("should count documents matching the query")
238+
.isEqualTo(1L);
239+
softly.assertThatThrownBy(() -> entityManager.count((SelectQuery) null))
240+
.as("must not accept null query")
241+
.isInstanceOf(NullPointerException.class);
242+
});
243+
}
244+
228245
@Test
229246
void shouldReadFromDifferentBaseDocumentUsingInstance() {
230247
entityManager.insert(getEntity());
@@ -273,7 +290,7 @@ void shouldInsertNull() {
273290
entity.add(Element.of("name", null));
274291
CommunicationEntity documentEntity = entityManager.insert(entity);
275292
Optional<Element> name = documentEntity.find("name");
276-
SoftAssertions.assertSoftly(soft -> {
293+
assertSoftly(soft -> {
277294
soft.assertThat(name).isPresent();
278295
soft.assertThat(name).get().extracting(Element::name).isEqualTo("name");
279296
soft.assertThat(name).get().extracting(Element::get).isNull();
@@ -286,7 +303,7 @@ void shouldUpdateNull(){
286303
entity.add(Element.of("name", null));
287304
var documentEntity = entityManager.update(entity);
288305
Optional<Element> name = documentEntity.find("name");
289-
SoftAssertions.assertSoftly(soft -> {
306+
assertSoftly(soft -> {
290307
soft.assertThat(name).isPresent();
291308
soft.assertThat(name).get().extracting(Element::name).isEqualTo("name");
292309
soft.assertThat(name).get().extracting(Element::get).isNull();
@@ -361,7 +378,7 @@ void shouldInsertUUID() {
361378
entity.add("uuid", UUID.randomUUID());
362379
var documentEntity = entityManager.insert(entity);
363380
Optional<Element> uuid = documentEntity.find("uuid");
364-
SoftAssertions.assertSoftly(soft -> {
381+
assertSoftly(soft -> {
365382
soft.assertThat(uuid).isPresent();
366383
Element element = uuid.orElseThrow();
367384
soft.assertThat(element.name()).isEqualTo("uuid");
@@ -382,7 +399,7 @@ void shouldFindBetween() {
382399

383400
var result = entityManager.select(query).toList();
384401

385-
SoftAssertions.assertSoftly(softly -> {
402+
assertSoftly(softly -> {
386403
softly.assertThat(result).hasSize(2);
387404
softly.assertThat(result).map(e -> e.find("age").orElseThrow().get(Integer.class)).contains(22, 23);
388405
softly.assertThat(result).map(e -> e.find("age").orElseThrow().get(Integer.class)).doesNotContain(25);
@@ -402,7 +419,7 @@ void shouldFindBetween2() {
402419

403420
var result = entityManager.select(query).toList();
404421

405-
SoftAssertions.assertSoftly(softly -> {
422+
assertSoftly(softly -> {
406423
softly.assertThat(result).hasSize(2);
407424
softly.assertThat(result).map(e -> e.find("age").orElseThrow().get(Integer.class)).contains(22, 23);
408425
softly.assertThat(result).map(e -> e.find("age").orElseThrow().get(Integer.class)).doesNotContain(25);
@@ -418,7 +435,7 @@ void shouldFindContains() {
418435
"lia")), COLLECTION_NAME, Collections.emptyList());
419436

420437
var result = entityManager.select(query).toList();
421-
SoftAssertions.assertSoftly(softly -> {
438+
assertSoftly(softly -> {
422439
softly.assertThat(result).hasSize(1);
423440
softly.assertThat(result.get(0).find("name").orElseThrow().get(String.class)).isEqualTo("Poliana");
424441
});
@@ -433,7 +450,7 @@ void shouldStartsWith() {
433450
"Pol")), COLLECTION_NAME, Collections.emptyList());
434451

435452
var result = entityManager.select(query).toList();
436-
SoftAssertions.assertSoftly(softly -> {
453+
assertSoftly(softly -> {
437454
softly.assertThat(result).hasSize(1);
438455
softly.assertThat(result.get(0).find("name").orElseThrow().get(String.class)).isEqualTo("Poliana");
439456
});
@@ -448,7 +465,7 @@ void shouldEndsWith() {
448465
"ana")), COLLECTION_NAME, Collections.emptyList());
449466

450467
var result = entityManager.select(query).toList();
451-
SoftAssertions.assertSoftly(softly -> {
468+
assertSoftly(softly -> {
452469
softly.assertThat(result).hasSize(1);
453470
softly.assertThat(result.get(0).find("name").orElseThrow().get(String.class)).isEqualTo("Poliana");
454471
});
@@ -481,7 +498,7 @@ void shouldCreateEdge() {
481498
);
482499

483500
var result = entityManager.aql(aql, parameters).toList();
484-
SoftAssertions.assertSoftly(softly -> softly.assertThat(result).isNotEmpty());
501+
assertSoftly(softly -> softly.assertThat(result).isNotEmpty());
485502

486503
entityManager.remove(person1, "FRIEND", person2);
487504
}
@@ -504,7 +521,7 @@ void shouldRemoveEdge() {
504521
""";
505522
Map<String, Object> parameters = Map.of("edgeId", edge.id());
506523
var result = entityManager.aql(aql, parameters).toList();
507-
SoftAssertions.assertSoftly(softly -> softly.assertThat(result).isEmpty());
524+
assertSoftly(softly -> softly.assertThat(result).isEmpty());
508525
}
509526

510527
@Test
@@ -520,7 +537,7 @@ void shouldDeleteEdgeById() {
520537
""";
521538
Map<String, Object> parameters = Map.of("id", edge.id());
522539
var result = entityManager.aql(aql, parameters).toList();
523-
SoftAssertions.assertSoftly(softly -> softly.assertThat(result).isEmpty());
540+
assertSoftly(softly -> softly.assertThat(result).isEmpty());
524541
}
525542

526543
@Test
@@ -532,7 +549,7 @@ void shouldFindEdgeById() {
532549
var edgeId = edge.id();
533550
var retrievedEdge = entityManager.findEdgeById(edgeId);
534551

535-
SoftAssertions.assertSoftly(softly -> {
552+
assertSoftly(softly -> {
536553
softly.assertThat(retrievedEdge).isPresent();
537554
softly.assertThat(retrievedEdge.get().label()).isEqualTo("FRIEND");
538555
softly.assertThat(retrievedEdge.get().properties()).containsEntry("since", 2020);
@@ -555,7 +572,7 @@ void shouldCreateEdgeWithProperties() {
555572
Map<String, Object> parameters = Map.of("edgeId", edge.id());
556573

557574
var result = entityManager.aql(aql, parameters).toList();
558-
SoftAssertions.assertSoftly(softly -> {
575+
assertSoftly(softly -> {
559576
softly.assertThat(result).isNotEmpty();
560577
softly.assertThat(edge.properties()).containsEntry("since", 2019);
561578
softly.assertThat(edge.properties()).containsEntry("strength", "strong");

jnosql-arangodb/src/test/java/org/eclipse/jnosql/databases/arangodb/communication/QueryAQLConverterTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,18 @@ public void shouldNegate() {
155155

156156
}
157157

158+
@Test
159+
public void shouldRunEqualsCountQuery() {
160+
SelectQuery query = select().from("collection")
161+
.where("name").eq("value").build();
162+
163+
AQLQueryResult convert = QueryAQLConverter.count(query);
164+
String aql = convert.query();
165+
Map<String, Object> values = convert.values();
166+
assertEquals("value", values.get("name"));
167+
assertEquals("RETURN LENGTH(FOR c IN collection FILTER c.name == @name RETURN c)", aql);
168+
169+
}
170+
171+
158172
}

jnosql-cassandra/src/main/java/org/eclipse/jnosql/databases/cassandra/communication/CassandraColumnManager.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ public interface CassandraColumnManager extends DatabaseManager {
101101
*/
102102
Stream<CommunicationEntity> select(SelectQuery query, ConsistencyLevel level) throws NullPointerException;
103103

104+
/**
105+
* Count based on a query using a consistency level
106+
*
107+
* @param query the query
108+
* @param level the consistency level
109+
* @return the query using a consistency level
110+
* @throws NullPointerException when either query or level are null
111+
*/
112+
long count(SelectQuery query, ConsistencyLevel level) throws NullPointerException;
113+
114+
104115
/**
105116
* Executes CQL
106117
*

jnosql-cassandra/src/main/java/org/eclipse/jnosql/databases/cassandra/communication/DefaultCassandraColumnManager.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,20 @@ public long count(String columnFamily) {
159159
return execute.one().getLong(0);
160160
}
161161

162+
@Override
163+
public long count(SelectQuery query) {
164+
requireNonNull(query, "query is required");
165+
QueryExecutor executor = QueryExecutor.of(query);
166+
return executor.count(keyspace, query, this);
167+
}
168+
169+
@Override
170+
public long count(SelectQuery query, ConsistencyLevel level) {
171+
requireNonNull(query, "query is required");
172+
requireNonNull(level, "level is required");
173+
QueryExecutor executor = QueryExecutor.of(query);
174+
return executor.count(keyspace, query, level, this);
175+
}
162176

163177
@Override
164178
public void close() {

jnosql-cassandra/src/main/java/org/eclipse/jnosql/databases/cassandra/communication/QueryExecutor.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,17 @@ static QueryExecutor of(SelectQuery query) {
2929
return QueryExecutorType.DEFAULT;
3030
}
3131

32-
Stream<CommunicationEntity> execute(String keyspace, SelectQuery query, DefaultCassandraColumnManager manager);
32+
default Stream<CommunicationEntity> execute(String keyspace, SelectQuery query, DefaultCassandraColumnManager manager) {
33+
return execute(keyspace, query, null, manager);
34+
}
3335

3436
Stream<CommunicationEntity> execute(String keyspace, SelectQuery query, ConsistencyLevel level,
35-
DefaultCassandraColumnManager manager);
37+
DefaultCassandraColumnManager manager);
38+
39+
default long count(String keyspace, SelectQuery query, DefaultCassandraColumnManager manager) {
40+
return count(keyspace, query, null, manager);
41+
}
42+
43+
long count(String keyspace, SelectQuery query, ConsistencyLevel level, DefaultCassandraColumnManager manager);
3644

3745
}

jnosql-cassandra/src/main/java/org/eclipse/jnosql/databases/cassandra/communication/QueryExecutorType.java

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,9 @@
3030

3131
enum QueryExecutorType implements QueryExecutor {
3232
PAGING_STATE {
33-
@Override
34-
public Stream<CommunicationEntity> execute(String keyspace, SelectQuery query, DefaultCassandraColumnManager manager) {
35-
return execute(keyspace, query, null, manager);
36-
}
37-
3833
@Override
3934
public Stream<CommunicationEntity> execute(String keyspace, SelectQuery q, ConsistencyLevel level,
40-
DefaultCassandraColumnManager manager) {
35+
DefaultCassandraColumnManager manager) {
4136

4237
CassandraQuery query = CassandraQuery.class.cast(q);
4338

@@ -73,14 +68,9 @@ public Stream<CommunicationEntity> execute(String keyspace, SelectQuery q, Consi
7368

7469
},
7570
DEFAULT {
76-
@Override
77-
public Stream<CommunicationEntity> execute(String keyspace, SelectQuery query, DefaultCassandraColumnManager manager) {
78-
return execute(keyspace, query, null, manager);
79-
}
80-
8171
@Override
8272
public Stream<CommunicationEntity> execute(String keyspace, SelectQuery query, ConsistencyLevel level,
83-
DefaultCassandraColumnManager manager) {
73+
DefaultCassandraColumnManager manager) {
8474

8575
Select cassandraSelect = QueryUtils.select(query, keyspace);
8676

@@ -98,5 +88,17 @@ public Stream<CommunicationEntity> execute(String keyspace, SelectQuery query, C
9888
}
9989
return resultSet.all().stream().map(CassandraConverter::toDocumentEntity);
10090
}
91+
};
92+
93+
@Override
94+
public long count(String keyspace, SelectQuery query, ConsistencyLevel level, DefaultCassandraColumnManager manager) {
95+
96+
Select cassandraSelect = QueryUtils.select(query, keyspace).countAll();
97+
SimpleStatement select = cassandraSelect.build();
98+
if (Objects.nonNull(level)) {
99+
select = select.setConsistencyLevel(level);
100+
}
101+
ResultSet resultSet = manager.getSession().execute(select);
102+
return resultSet.one().getLong(0);
101103
}
102104
}

jnosql-cassandra/src/main/java/org/eclipse/jnosql/databases/cassandra/mapping/CassandraTemplate.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ public interface CassandraTemplate extends ColumnTemplate {
8787
*/
8888
void delete(DeleteQuery query, ConsistencyLevel level);
8989

90+
/**
91+
* Returns the count of records that match the given {@link SelectQuery} using the specified
92+
* {@link ConsistencyLevel}.
93+
*
94+
* @param query the select query defining the criteria for counting; must not be {@code null}
95+
* @param level the Cassandra consistency level to use for the operation; must not be {@code null}
96+
* @return the number of records matching the query
97+
* @throws NullPointerException if {@code query} or {@code level} is {@code null}
98+
*/
99+
long count(SelectQuery query, ConsistencyLevel level);
100+
90101
/**
91102
* Executes a {@link SelectQuery} using a specified {@link ConsistencyLevel} and retrieves the matching records.
92103
*

0 commit comments

Comments
 (0)