Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,23 @@ public interface CriteriaBuilder extends PredicateFactory {
<N extends Number> Expression<N> floor(Expression<N> x);

/**
* Create an aggregate expression applying the count operation. Return type of count function in SPARQL is
* xsd:integer which JOPA internally represents as Integer.
* Create an aggregate expression applying the count operation.
*
* @param x expression representing input value to count operation
* @return count expression
* @implNote Return type of count function in SPARQL is xsd:integer which JOPA internally represents as Integer.
*/
Expression<Integer> count(Expression<?> x);

/**
* Create an aggregate expression applying the count distinct operation.
*
* @param x expression representing input value to count operation
* @return count distinct expression
* @implNote Return type of count function in SPARQL is xsd:integer which JOPA internally represents as Integer.
*/
Expression<Integer> countDistinct(Expression<?> x);

/**
* Create expression to return length of a string.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import cz.cvut.kbss.jopa.query.criteria.expressions.AbstractExpression;
import cz.cvut.kbss.jopa.query.criteria.expressions.AbstractPathExpression;
import cz.cvut.kbss.jopa.query.criteria.expressions.CeilFunction;
import cz.cvut.kbss.jopa.query.criteria.expressions.CountDistinctFunction;
import cz.cvut.kbss.jopa.query.criteria.expressions.CountFunction;
import cz.cvut.kbss.jopa.query.criteria.expressions.ExpressionEqualImpl;
import cz.cvut.kbss.jopa.query.criteria.expressions.ExpressionGreaterThanImpl;
Expand Down Expand Up @@ -84,6 +85,12 @@ public Expression<Integer> count(Expression<?> x) {
return new CountFunction((AbstractPathExpression) x, this);
}

@Override
public Expression<Integer> countDistinct(Expression<?> x) {
validateFunctionArgument(x);
return new CountDistinctFunction((AbstractPathExpression) x, this);
}

@Override
public <N extends Number> Expression<N> ceil(Expression<N> x) {
validateFunctionArgument(x);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@ public AbstractFunctionExpression(Class<X> type, CriteriaBuilder cb, AbstractPat
@Override
public void setExpressionToQuery(StringBuilder query, CriteriaParameterFiller parameterFiller) {
query.append(getFunctionName()).append("(");
constructFunctionArguments(query, parameterFiller);
query.append(")");
}

protected void constructFunctionArguments(StringBuilder query, CriteriaParameterFiller parameterFiller) {
for (int i = 0; i < argumentExpression.size(); i++) {
argumentExpression.get(i).setExpressionToQuery(query, parameterFiller);
if (i < argumentExpression.size() - 1) {
query.append(',');
}
}
query.append(")");
}

public abstract String getFunctionName();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package cz.cvut.kbss.jopa.query.criteria.expressions;

import cz.cvut.kbss.jopa.model.query.criteria.CriteriaBuilder;
import cz.cvut.kbss.jopa.query.criteria.CriteriaParameterFiller;

public class CountDistinctFunction extends CountFunction {

public CountDistinctFunction(AbstractPathExpression expression, CriteriaBuilder cb) {
super(expression, cb);
}

@Override
protected void constructFunctionArguments(StringBuilder query, CriteriaParameterFiller parameterFiller) {
query.append("DISTINCT ");
super.constructFunctionArguments(query, parameterFiller);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* JOPA
* Copyright (C) 2026 Czech Technical University in Prague
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library.
*/
package cz.cvut.kbss.jopa.query.soql;

/**
* Aggregate functions supported in SOQL select expressions.
*/
enum AggregateFunction {

COUNT(SoqlConstants.Functions.COUNT, "?count");

private final String soqlName;
private final String resultVariable;

AggregateFunction(String soqlName, String resultVariable) {
this.soqlName = soqlName;
this.resultVariable = resultVariable;
}

String soqlName() {
return soqlName;
}

String resultVariable() {
return resultVariable;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cz.cvut.kbss.jopa.query.soql;

/**
* Captures the kind of projection in the SOQL SELECT clause.
*
* @param aggregateFunction the aggregate function wrapping the projected variable, or {@code null} for a plain
* projection
* @param distinct whether the outer {@code SELECT DISTINCT} modifier is present
* @param aggregateDistinct whether {@code DISTINCT} is present inside the aggregate argument
*/
record SelectProjection(AggregateFunction aggregateFunction, boolean distinct, boolean aggregateDistinct) {

SelectProjection withAggregateFunction(AggregateFunction fn) {
return new SelectProjection(fn, distinct, aggregateDistinct);
}

SelectProjection withDistinct(boolean value) {
return new SelectProjection(aggregateFunction, value, aggregateDistinct);
}

SelectProjection withAggregateDistinct(boolean value) {
return new SelectProjection(aggregateFunction, distinct, value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,7 @@ public class SoqlQueryListener extends SoqlBaseListener {

private final Map<FieldSpecification<?, ?>, TriplePatternEnhancer> tpEnhancers = new HashMap<>();

private boolean isSelectedParamDistinct = false;

private boolean isSelectedParamCount = false;
private SelectProjection selectProjection = new SelectProjection(null, false, false);

private boolean isInObjectIdentifierExpression = false;

Expand Down Expand Up @@ -155,9 +153,7 @@ private SoqlNode linkSimpleSubpath(ParserRuleContext ctx) {
public void enterSelectClause(SoqlParser.SelectClauseContext ctx) {
this.typeDef = QueryType.SELECT.getKeyword();

if (ctx.DISTINCT() != null) {
this.isSelectedParamDistinct = true;
}
this.selectProjection = selectProjection.withDistinct(ctx.DISTINCT() != null);
}

private void pushNewAttribute(SoqlAttribute myAttr) {
Expand All @@ -175,18 +171,16 @@ private void popAttribute() {

@Override
public void exitSelectExpression(SoqlParser.SelectExpressionContext ctx) {
if (!isSelectedParamCount) {
if (selectProjection.aggregateFunction() == null) {
this.projectedVariable = ctx.getText();
}
}

@Override
public void enterAggregateExpression(SoqlParser.AggregateExpressionContext ctx) {
if (ctx.COUNT() != null) {
isSelectedParamCount = true;
if (ctx.DISTINCT() != null) {
isSelectedParamDistinct = true;
}
selectProjection = selectProjection.withAggregateFunction(AggregateFunction.COUNT)
.withAggregateDistinct(ctx.DISTINCT() != null);

if (ctx.simpleSubpath() != null) {
this.projectedVariable = ctx.simpleSubpath().getText();
Expand Down Expand Up @@ -550,10 +544,13 @@ private void buildSparqlQueryString() {
String selectParameter = getSelectParameter(attributes.get(0));

StringBuilder newQueryBuilder = new StringBuilder(typeDef);
if (isSelectedParamCount) {
newQueryBuilder.append(getCountPart(selectParameter));
if (selectProjection.aggregateFunction() != null) {
if (selectProjection.distinct()) {
newQueryBuilder.append(' ').append(SoqlConstants.DISTINCT);
}
newQueryBuilder.append(getAggregatePart(selectParameter));
} else {
if (isSelectedParamDistinct) {
if (selectProjection.distinct()) {
newQueryBuilder.append(' ').append(SoqlConstants.DISTINCT);
}
newQueryBuilder.append(' ').append(selectParameter).append(' ');
Expand All @@ -578,13 +575,14 @@ private void buildSparqlQueryString() {
LOG.trace("Translated SOQL query '{}' to SPARQL '{}'.", soql, sparql);
}

private StringBuilder getCountPart(String selectParameter) {
StringBuilder countPart = new StringBuilder(" (COUNT(");
if (isSelectedParamDistinct) {
countPart.append(SoqlConstants.DISTINCT).append(' ');
private StringBuilder getAggregatePart(String selectParameter) {
final AggregateFunction fn = selectProjection.aggregateFunction();
final StringBuilder aggregatePart = new StringBuilder(" (").append(fn.soqlName()).append('(');
if (selectProjection.aggregateDistinct()) {
aggregatePart.append(SoqlConstants.DISTINCT).append(' ');
}
countPart.append(selectParameter).append(") AS ?count) ");
return countPart;
aggregatePart.append(selectParameter).append(") AS ").append(fn.resultVariable()).append(") ");
return aggregatePart;
}

private StringBuilder processSupremeAttributes() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ public void testTranslateQueryDistinctCount() {
assertEquals(expectedSoqlQuery, generatedSoqlQuery);
}

@Test
void testTranslationQueryCountDistinct() {
CriteriaQueryImpl<Integer> query = cb.createQuery(Integer.class);
Root<OWLClassA> root = query.from(OWLClassA.class);
query.select(cb.countDistinct(root));

final String generatedSoqlQuery = query.translateQuery(criteriaParameterFiller);
final String expectedSoqlQuery = "SELECT COUNT(DISTINCT owlclassa) FROM OWLClassA owlclassa";
assertEquals(expectedSoqlQuery, generatedSoqlQuery);
}

@Test
public void testTranslateQuerySelectAllOrderByEntityDesc() {
CriteriaQueryImpl<OWLClassA> query = cb.createQuery(OWLClassA.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ public void testParseCountQuery() {
@Test
public void testParseDistinctCountQuery() {
final String soqlQuery = "SELECT DISTINCT COUNT(p) FROM Person p";
final String expectedSparqlQuery =
"SELECT DISTINCT (COUNT(?x) AS ?count) WHERE { ?x a " + strUri(Vocabulary.c_Person) + " . }";
final QueryHolder holder = sut.parseQuery(soqlQuery);
assertEquals(expectedSparqlQuery, holder.getQuery());
assertEquals(2, holder.getParameters().size());
}

@Test
void testParseCountDistinctQuery() {
final String soqlQuery = "SELECT COUNT(DISTINCT p) FROM Person p";
final String expectedSparqlQuery =
"SELECT (COUNT(DISTINCT ?x) AS ?count) WHERE { ?x a " + strUri(Vocabulary.c_Person) + " . }";
final QueryHolder holder = sut.parseQuery(soqlQuery);
Expand Down Expand Up @@ -769,8 +779,8 @@ void parseQuerySupportsCountWithProjectedAttribute() {
}

@Test
void parseQuerySupportsDistinctCountWithProjectedAttribute() {
final String soqlIdFirst = "SELECT DISTINCT COUNT(d.owlClassA) FROM OWLClassD d WHERE d.uri = :uri";
void parseQuerySupportsCountDistinctWithProjectedAttribute() {
final String soqlIdFirst = "SELECT COUNT(DISTINCT d.owlClassA) FROM OWLClassD d WHERE d.uri = :uri";
final String expectedSparql = "SELECT (COUNT(DISTINCT ?owlClassA) AS ?count) WHERE { ?uri a " + strUri(Vocabulary.c_OwlClassD) + " . " +
"?uri " + strUri(Vocabulary.p_h_hasA) + " ?owlClassA . }";
parseAndAssertEquality(expectedSparql, soqlIdFirst);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,11 @@ public void querySupportsNamedFetchGraphs() {
public void querySupportsNestedNamedFetchGraphs() {
// OWL2Query does not support OPTIONAL
}

@Disabled
@Override
public void querySupportsMappingResultsDirectlyToEntity() {
// OWL2Query does not seem to correctly implement ORDER BY, causing result rows not always to be consecutive by subject identifier
// And thus the query sometimes fails
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ public void querySupportsMappingResultsDirectlyToEntity() {
?x a ?type ;
?hasString ?stringAttribute;
a ?types .
}""", OWLClassA.class)
} ORDER BY ?x""", OWLClassA.class)
.setParameter("type", URI.create(Vocabulary.C_OWL_CLASS_A))
.setParameter("hasString", URI.create(Vocabulary.P_A_STRING_ATTRIBUTE))
.getResultList();
Expand Down
18 changes: 18 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${maven.javadoc.plugin.version}</version>
<configuration>
<tags>
<tag>
<name>implNote</name>
<placement>a</placement>
<head>Implementation Note:</head>
</tag>
</tags>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
Expand Down Expand Up @@ -175,6 +184,15 @@
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${maven.javadoc.plugin.version}</version>
<configuration>
<tags>
<tag>
<name>implNote</name>
<placement>a</placement>
<head>Implementation Note:</head>
</tag>
</tags>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
Expand Down
Loading