CustomAggregateFunctionEvaluationTest.java

/*******************************************************************************
 * Copyright (c) 2022 Eclipse RDF4J contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
package org.eclipse.rdf4j.query.parser.sparql.function;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.io.StringReader;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.base.CoreDatatype;
import org.eclipse.rdf4j.model.datatypes.XMLDatatypeUtil;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.query.MalformedQueryException;
import org.eclipse.rdf4j.query.QueryEvaluationException;
import org.eclipse.rdf4j.query.TupleQuery;
import org.eclipse.rdf4j.query.TupleQueryResult;
import org.eclipse.rdf4j.query.algebra.MathExpr;
import org.eclipse.rdf4j.query.algebra.evaluation.ValueExprEvaluationException;
import org.eclipse.rdf4j.query.algebra.evaluation.function.aggregate.stdev.PopulationStandardDeviationAggregateFactory;
import org.eclipse.rdf4j.query.algebra.evaluation.function.aggregate.stdev.StandardDeviationAggregateFactory;
import org.eclipse.rdf4j.query.algebra.evaluation.function.aggregate.variance.PopulationVarianceAggregateFactory;
import org.eclipse.rdf4j.query.algebra.evaluation.function.aggregate.variance.VarianceAggregateFactory;
import org.eclipse.rdf4j.query.algebra.evaluation.util.MathUtil;
import org.eclipse.rdf4j.query.parser.sparql.aggregate.AggregateCollector;
import org.eclipse.rdf4j.query.parser.sparql.aggregate.AggregateFunction;
import org.eclipse.rdf4j.query.parser.sparql.aggregate.AggregateFunctionFactory;
import org.eclipse.rdf4j.query.parser.sparql.aggregate.CustomAggregateFunctionRegistry;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.repository.sail.SailRepository;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.sail.memory.MemoryStore;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

/**
 * Integration tests for evaluation of custom aggregate functions in SPARQL
 *
 * @author Tomas Kovachev t.kovachev1996@gmail.com
 */
public class CustomAggregateFunctionEvaluationTest {

	private static SailRepository rep;

	private static AggregateFunctionFactory functionFactory;

	@BeforeAll
	public static void setUp() throws IOException {
		rep = new SailRepository(new MemoryStore());
		functionFactory = new AggregateFunctionFactory() {
			@Override
			public String getIri() {
				return "https://www.rdf4j.org/aggregate#x";
			}

			@Override
			public AggregateFunction<SumCollector, Value> buildFunction(Function<BindingSet, Value> evaluationStep) {
				return new AggregateFunction<>(evaluationStep) {

					public void processAggregate(BindingSet s, Predicate<Value> distinctValue, SumCollector sum)
							throws QueryEvaluationException {
						if (sum.typeError != null) {
							// halt further processing if a type error has been raised
							return;
						}

						Value v = evaluate(s);
						if (v instanceof Literal) {
							if (distinctValue.test(v)) {
								Literal nextLiteral = (Literal) v;
								if (nextLiteral.getDatatype() != null
										&& XMLDatatypeUtil.isNumericDatatype(nextLiteral.getDatatype())) {
									sum.value = MathUtil.compute(sum.value, nextLiteral, MathExpr.MathOp.PLUS);
								} else {
									sum.typeError = new ValueExprEvaluationException("not a number: " + v);
								}
							}
						}
					}
				};
			}

			@Override
			public SumCollector getCollector() {
				return new SumCollector();
			}
		};
		CustomAggregateFunctionRegistry.getInstance().add(functionFactory);
		// add data to avoid processing it every time
		addData();
	}

	@AfterAll
	public static void cleanUp() {
		CustomAggregateFunctionRegistry.getInstance().remove(functionFactory);
	}

	@Test
	public void testCustomFunction_Sum() {
		String query = "select (<" + functionFactory.getIri() + ">(?o) as ?m) where { \n"
				+ "\t?s <urn:n> ?o . filter(?o > 0) }";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				BindingSet bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("125.933564200001");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_Stdev_Population() {
		String query = "select (<" + new PopulationStandardDeviationAggregateFactory().getIri()
				+ ">(?o) as ?m) where { \n"
				+ "\t?s <urn:n> ?o . filter(?o > 0) }";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				BindingSet bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("12.194600810623243");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_Stdev_Default() {
		String query = "select (<" + new StandardDeviationAggregateFactory().getIri() + ">(?o) as ?m) where { \n"
				+ "\t?s <urn:n> ?o . filter(?o > 0) }";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				BindingSet bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("13.0365766290937");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_StdevEmpty() {
		String query = "select (<" + new StandardDeviationAggregateFactory().getIri() + ">(?o) as ?m) where { \n"
				+ "\t?s <urn:n3> ?o . filter(?o > 0) }";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				BindingSet bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_Variance() {
		String query = "select (<" + new VarianceAggregateFactory().getIri() + ">(?o) as ?m) where { \n"
				+ "\t?s <urn:n> ?o . filter(?o > 0) }";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				BindingSet bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("169.95233020623206");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_VariancePopulation() {
		String query = "select (<" + new PopulationVarianceAggregateFactory().getIri() + ">(?o) as ?m) where { \n"
				+ "\t?s <urn:n> ?o . filter(?o > 0) }";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				BindingSet bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("148.70828893045305");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_VarianceEmpty() {
		String query = "select (<" + new VarianceAggregateFactory().getIri() + ">(?o) as ?m) where { \n"
				+ "\t?s <urn:n3> ?o . filter(?o > 0) }";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				BindingSet bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_VarianceWithDistinct() {
		String query = "select (<" + new PopulationVarianceAggregateFactory().getIri()
				+ ">(distinct ?o) as ?m) ?s where { \n"
				+ "\t ?s ?p ?o . } group by ?s ";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				BindingSet bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book8");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.002500019073522708");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book1");
				bs = result.next();
				assertThat(bs.getValue("m")).isNull();
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book5");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book4");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book6");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("1.0572322555240015");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book7");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book3");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book2");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_VarianceWithDistinct_WithStdv() {
		String query = "select (<" + new PopulationVarianceAggregateFactory().getIri() + ">(distinct ?o) as ?m) (<"
				+ new StandardDeviationAggregateFactory().getIri() + ">(?o) as ?n) ?s where { \n"
				+ "\t ?s ?p ?o . } group by ?s ";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				var bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("n").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book3");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("n").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book2");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("n").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book4");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.002500019073522708");
				assertThat(bs.getValue("n").stringValue()).isEqualTo("0.057735247160611895");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book1");
				bs = result.next();
				assertThat(bs.getValue("m")).isNull();
				assertThat(bs.getValue("n")).isNull();
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book5");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("n").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book6");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("n").stringValue()).isEqualTo("0.0");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book8");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("1.0572322555240015");
				assertThat(bs.getValue("n").stringValue()).isEqualTo("1.45411984067614");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book7");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_SumWithDistinct() {
		String query = "select (<" + functionFactory.getIri() + ">(distinct ?o) as ?m) ?s where { \n"
				+ "\t ?s <urn:n> ?o . } group by ?s ";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				BindingSet bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("12.5");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book1");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("12.5");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book3");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("3");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book8");
				bs = result.next();
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book5");
				assertThat(bs.getValue("m")).isNull();
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("60.543564");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book7");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.090000200001");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book6");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("6");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book2");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("31.3");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book4");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_MultipleSum() {
		String query = "select (<" + functionFactory.getIri() + ">(?o) as ?m) (sum(?o) as ?sa) where { \n"
				+ "\t?s <urn:n> ?o . filter(?o > 0) }";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				BindingSet bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo(bs.getValue("sa").stringValue());
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_MultipleSumWithDistinct() {
		String query = "select (<" + functionFactory.getIri() + ">(distinct ?o) as ?m) (sum(?o) as ?sa) ?s where { \n"
				+ "\t?s ?p ?o . filter(?o > 0) } group by ?s";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				BindingSet bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("12.5");
				assertThat(bs.getValue("sa").stringValue()).isEqualTo("12.5");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book3");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("6");
				assertThat(bs.getValue("sa").stringValue()).isEqualTo("6");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book2");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("311");
				assertThat(bs.getValue("sa").stringValue()).isEqualTo("311");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book5");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("25.1");
				assertThat(bs.getValue("sa").stringValue()).isEqualTo("37.6");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book1");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("31.3");
				assertThat(bs.getValue("sa").stringValue()).isEqualTo("31.3");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book4");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("60.543564");
				assertThat(bs.getValue("sa").stringValue()).isEqualTo("60.543564");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book7");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("0.090000200001");
				assertThat(bs.getValue("sa").stringValue()).isEqualTo("0.090000200001");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book6");
				bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("3");
				assertThat(bs.getValue("sa").stringValue()).isEqualTo("3");
				assertThat(bs.getValue("s").stringValue()).isEqualTo("http://example/book8");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomAndStandardSum_WithFilter() {
		String query = "select (<" + functionFactory.getIri() + ">(distinct ?o) as ?m) (sum(?o) as ?sa) where { \n"
				+ "\t?s ?p ?o . filter(?o > 0) }";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				BindingSet bs = result.next();
				assertThat(bs.getValue("m").stringValue()).isEqualTo("418.2335645814707");
				assertThat(bs.getValue("sa").stringValue()).isEqualTo("462.0335626741221");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_InvalidValues() {
		String query = "select (<" + functionFactory.getIri() + ">(distinct ?o) as ?m) (sum(?o) as ?sa) where { \n"
				+ "\t?s ?p ?o .  }";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				assertThat(result.next().size()).isEqualTo(0);
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_MultipleSumWithDistinctGroupBy() {
		String query = "select ?s (<" + functionFactory.getIri() + ">(distinct ?o) as ?m) where { \n"

				+ "\t?s <urn:n> ?o . filter(?o > 0) } group by ?s order by ?s";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				assertThat(result.next().getValue("m").stringValue()).isEqualTo("12.5");
				assertThat(result.next().getValue("m").stringValue()).isEqualTo("6");
				assertThat(result.next().getValue("m").stringValue()).isEqualTo("12.5");
				assertThat(result.next().getValue("m").stringValue()).isEqualTo("31.3");
				assertThat(result.next().getValue("m").stringValue()).isEqualTo("0.090000200001");
				assertThat(result.next().getValue("m").stringValue()).isEqualTo("60.543564");
				assertThat(result.next().getValue("m").stringValue()).isEqualTo("3");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testCustomFunction_MultipleSumWithHaving() {
		String query = "select ?s (<" + functionFactory.getIri() + ">(?o) as ?m) where { \n"
				+ "\t?s <urn:n> ?o . }\n" +
				"GROUP BY ?s \n" +
				"HAVING((<" + functionFactory.getIri() + ">( ?o)) > 60)";
		try (RepositoryConnection conn = rep.getConnection()) {
			TupleQuery tupleQuery = conn.prepareTupleQuery(query);
			try (TupleQueryResult result = tupleQuery.evaluate()) {
				assertThat(result.next().getValue("s").stringValue()).isEqualTo("http://example/book7");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testSumWithHavingFilter() {
		String query = "select ?s (SUM(?o) as ?sum)  where { \n"
				+ "\t?s <urn:n> ?o . }\n" +
				"GROUP BY ?s \n" +
				"HAVING(SUM(?o) > 60)";
		try (RepositoryConnection conn = rep.getConnection()) {
			TupleQuery tupleQuery = conn.prepareTupleQuery(query);
			try (TupleQueryResult result = tupleQuery.evaluate()) {
				assertThat(result.next().getValue("s").stringValue()).isEqualTo("http://example/book7");
				assertThat(result.hasNext()).isFalse();
			}
		}
	}

	@Test
	public void testAvgWithHavingFilter() {
		String query = "select ?s (avg(?o) as ?avg)  where { \n"
				+ "\t?s <urn:n> ?o . }\n" +
				"GROUP BY ?s \n" +
				"HAVING(avg(?o) > 30)";
		try (RepositoryConnection conn = rep.getConnection()) {
			TupleQuery tupleQuery = conn.prepareTupleQuery(query);
			try (TupleQueryResult result = tupleQuery.evaluate()) {
				var resultList = result.stream().collect(Collectors.toList());
				assertThat(resultList.size()).isEqualTo(2);
				assertThat(resultList.stream()
						.anyMatch(bs -> bs.getValue("s").stringValue().equals("http://example/book4"))).isTrue();
				assertThat(resultList.stream()
						.anyMatch(bs -> bs.getValue("s").stringValue().equals("http://example/book7"))).isTrue();
			}
		}
	}

	@Test
	public void testNonExistentCustomAggregateFunction() {
		String query = "select (<http://example.org/doesNotExist>(distinct ?o) as ?m) where { ?s <urn:n> ?o . }";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				QueryEvaluationException queryEvaluationException = Assertions
						.assertThrows(QueryEvaluationException.class, result::next);
				Assertions.assertTrue(queryEvaluationException.toString().contains("aggregate"));
			}
		}
	}

	@Test
	public void testNonExistentCustomAggregateFunction2() {
		String query = "select (<http://example.org/doesNotExist>(?o) as ?m) where { ?s <urn:n> ?o . }";
		try (RepositoryConnection conn = rep.getConnection()) {
			try (TupleQueryResult result = conn.prepareTupleQuery(query).evaluate()) {
				fail();
			} catch (QueryEvaluationException queryEvaluationException) {
				Assertions.assertFalse(queryEvaluationException.toString().contains("aggregate"));
			}
		}
	}

	@Test
	public void testNonExistentCustomAggregateFunction3() {
		String query = "select ?s (<http://example.org/doesNotExist>(?o) as ?m) where { ?s <urn:n> ?o . } group by ?s";
		try (RepositoryConnection conn = rep.getConnection()) {
			MalformedQueryException queryEvaluationException = Assertions.assertThrows(MalformedQueryException.class,
					() -> {
						TupleQuery tupleQuery = conn.prepareTupleQuery(query);
						try (TupleQueryResult result = tupleQuery.evaluate()) {
							result.next();
						}
					});
			Assertions.assertTrue(queryEvaluationException.toString().contains("non-aggregate"));

		}
	}

	private static void addData() throws IOException {
		try (RepositoryConnection conn = rep.getConnection()) {
			conn.add(new StringReader(" <http://example/book1> <urn:n> \"12.5\"^^xsd:float .\n"
					+ "    <http://example/book1> <urn:n1> \"12.5\"^^xsd:float .\n"
					+ "    <http://example/book1> <urn:n1> \"12.6\"^^xsd:float .\n"
					+ "    <http://example/book2> <urn:n> \"6\"^^xsd:int .\n"
					+ "    <http://example/book3> <urn:n>  \"12.5\"^^xsd:double .\n"
					+ "    <http://example/book4> <urn:n>  31.3 .\n"
					+ "    <http://example/book5> <urn:n>  \"311.11241cawda3\" .\n"
					+ "    <http://example/book5> <urn:n1> 311 .\n"
					+ "    <http://example/book6> <urn:n>  0.090000200001 .\n"
					+ "    <http://example/book7> <urn:n>  31.3 .\n"
					+ "    <http://example/book7> <urn:n>  29.243564 .\n"
					+ "    <http://example/book8> <urn:n>  3 ."), "", RDFFormat.TURTLE);
		}
	}

	/**
	 * Dummy collector to verify custom aggregate functions
	 */
	private static class SumCollector implements AggregateCollector {
		private ValueExprEvaluationException typeError = null;

		private Literal value = SimpleValueFactory.getInstance().createLiteral("0", CoreDatatype.XSD.INTEGER);

		@Override
		public Value getFinalValue() {
			if (typeError != null) {
				// a type error occurred while processing the aggregate, throw it now.
				throw typeError;
			}
			return value;
		}
	}
}