BuiltinFunctionTest.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.testsuite.sparql.tests;

import static org.eclipse.rdf4j.model.util.Values.iri;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.util.function.Supplier;
import java.util.stream.Stream;

import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.query.QueryEvaluationException;
import org.eclipse.rdf4j.query.QueryLanguage;
import org.eclipse.rdf4j.query.TupleQuery;
import org.eclipse.rdf4j.query.TupleQueryResult;
import org.eclipse.rdf4j.repository.Repository;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.testsuite.sparql.AbstractComplianceTest;
import org.junit.jupiter.api.DynamicTest;

/**
 * Tests on various SPARQL built-in functions.
 *
 * @author Jeen Broekstra
 *
 */
public class BuiltinFunctionTest extends AbstractComplianceTest {

	public BuiltinFunctionTest(Supplier<Repository> repo) {
		super(repo);
	}

	/**
	 * See https://github.com/eclipse/rdf4j/issues/1267
	 */

	private void testSeconds(RepositoryConnection conn) {
		String qry = "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> "
				+ "SELECT (SECONDS(\"2011-01-10T14:45:13\"^^xsd:dateTime) AS ?sec) { }";

		try (TupleQueryResult result = conn.prepareTupleQuery(QueryLanguage.SPARQL, qry).evaluate()) {
			assertNotNull(result);
			assertTrue(result.hasNext());
			assertEquals("13", result.next().getValue("sec").stringValue());
			assertFalse(result.hasNext());
		}
	}

	/**
	 * See https://github.com/eclipse/rdf4j/issues/1267
	 */

	private void testSecondsMilliseconds(RepositoryConnection conn) {
		String qry = "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> "
				+ "SELECT (SECONDS(\"2011-01-10T14:45:13.815-05:00\"^^xsd:dateTime) AS ?sec) { }";

		try (TupleQueryResult result = conn.prepareTupleQuery(QueryLanguage.SPARQL, qry).evaluate()) {
			assertNotNull(result);
			assertTrue(result.hasNext());
			assertEquals("13.815", result.next().getValue("sec").stringValue());
			assertFalse(result.hasNext());
		}
	}

	private void testSES1991NOWEvaluation(RepositoryConnection conn) throws Exception {
		loadTestData("/testdata-query/defaultgraph.ttl", conn);
		String query = "SELECT ?d WHERE {?s ?p ?o . BIND(NOW() as ?d) } LIMIT 2";

		TupleQuery tq = conn.prepareTupleQuery(QueryLanguage.SPARQL, query);

		try (TupleQueryResult result = tq.evaluate()) {
			assertNotNull(result);
			assertTrue(result.hasNext());
			Literal d1 = (Literal) result.next().getValue("d");
			assertTrue(result.hasNext());
			Literal d2 = (Literal) result.next().getValue("d");
			assertFalse(result.hasNext());
			assertNotNull(d1);
			assertEquals(d1, d2);
		} catch (QueryEvaluationException e) {
			e.printStackTrace();
			fail(e.getMessage());
		}
	}

	private void testSES869ValueOfNow(RepositoryConnection conn) {
		TupleQuery tq = conn.prepareTupleQuery(QueryLanguage.SPARQL,
				"SELECT ?p ( NOW() as ?n ) { BIND (NOW() as ?p ) }");

		try (TupleQueryResult result = tq.evaluate()) {
			assertNotNull(result);
			assertTrue(result.hasNext());

			BindingSet bs = result.next();
			Value p = bs.getValue("p");
			Value n = bs.getValue("n");

			assertNotNull(p);
			assertNotNull(n);
			assertEquals(p, n);
			assertTrue(p == n);
		}
	}

	private void testSES1991UUIDEvaluation(RepositoryConnection conn) throws Exception {
		loadTestData("/testdata-query/defaultgraph.ttl", conn);
		String query = "SELECT ?uid WHERE {?s ?p ?o . BIND(UUID() as ?uid) } LIMIT 2";

		TupleQuery tq = conn.prepareTupleQuery(QueryLanguage.SPARQL, query);

		try (TupleQueryResult result = tq.evaluate()) {
			assertNotNull(result);

			IRI uuid1 = (IRI) result.next().getValue("uid");
			IRI uuid2 = (IRI) result.next().getValue("uid");

			assertNotNull(uuid1);
			assertNotNull(uuid2);
			assertNotEquals(uuid1, uuid2);
		} catch (QueryEvaluationException e) {
			e.printStackTrace();
			fail(e.getMessage());
		}
	}

	private void testSES1991STRUUIDEvaluation(RepositoryConnection conn) throws Exception {
		loadTestData("/testdata-query/defaultgraph.ttl", conn);
		String query = "SELECT ?uid WHERE {?s ?p ?o . BIND(STRUUID() as ?uid) } LIMIT 2";

		TupleQuery tq = conn.prepareTupleQuery(QueryLanguage.SPARQL, query);

		try (TupleQueryResult result = tq.evaluate()) {
			assertNotNull(result);

			Literal uid1 = (Literal) result.next().getValue("uid");
			Literal uid2 = (Literal) result.next().getValue("uid");

			assertNotNull(uid1);
			assertNotEquals(uid1, uid2);
		} catch (QueryEvaluationException e) {
			e.printStackTrace();
			fail(e.getMessage());
		}
	}

	private void testSES1991RANDEvaluation(RepositoryConnection conn) throws Exception {
		loadTestData("/testdata-query/defaultgraph.ttl", conn);
		String query = "SELECT ?r WHERE {?s ?p ?o . BIND(RAND() as ?r) } LIMIT 3";

		TupleQuery tq = conn.prepareTupleQuery(QueryLanguage.SPARQL, query);

		try (TupleQueryResult result = tq.evaluate()) {
			assertNotNull(result);

			Literal r1 = (Literal) result.next().getValue("r");
			Literal r2 = (Literal) result.next().getValue("r");
			Literal r3 = (Literal) result.next().getValue("r");

			assertNotNull(r1);

			// there is a small chance that two successive calls to the random
			// number generator will generate the exact same value, so we check
			// for
			// three successive calls (still theoretically possible to be
			// identical, but phenomenally unlikely).
			assertFalse(r1.equals(r2) && r1.equals(r3));
		} catch (QueryEvaluationException e) {
			e.printStackTrace();
			fail(e.getMessage());
		}
	}

	private void testSES2121URIFunction(RepositoryConnection conn) {
		String query = "SELECT (URI(\"foo bar\") as ?uri) WHERE {}";
		TupleQuery tq = conn.prepareTupleQuery(QueryLanguage.SPARQL, query);
		try (TupleQueryResult result = tq.evaluate()) {
			assertNotNull(result);
			assertTrue(result.hasNext());
			BindingSet bs = result.next();
			IRI uri = (IRI) bs.getValue("uri");
			assertNull(uri, "uri result for invalid URI should be unbound");
		}

		query = "BASE <http://example.org/> SELECT (URI(\"foo bar\") as ?uri) WHERE {}";
		tq = conn.prepareTupleQuery(QueryLanguage.SPARQL, query);
		try (TupleQueryResult result = tq.evaluate()) {
			assertNotNull(result);
			assertTrue(result.hasNext());
			BindingSet bs = result.next();
			IRI uri = (IRI) bs.getValue("uri");
			assertNotNull(uri, "uri result for valid URI reference should be bound");
		}
	}

	private void test27NormalizeIRIFunction(RepositoryConnection conn) {

		String query = "SELECT (IRI(\"../bar\") as ?Iri) WHERE {}";
		TupleQuery tq = conn.prepareTupleQuery(QueryLanguage.SPARQL, query, "http://example.com/foo/");
		try (TupleQueryResult result = tq.evaluate()) {
			assertNotNull(result);
			assertTrue(result.hasNext());
			BindingSet bs = result.next();
			IRI actual = (IRI) bs.getValue("Iri");
			IRI expected = iri("http://example.com/bar");
			assertEquals(expected, actual, "IRI result for relative IRI should be normalized");
		}
	}

	private void testSES2052If1(RepositoryConnection conn) throws Exception {
		loadTestData("/testdata-query/dataset-query.trig", conn);
		String query = "SELECT ?p \n" +
				"WHERE { \n" +
				"         ?s ?p ?o . \n" +
				"        FILTER(IF(BOUND(?p), ?p = <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>, false)) \n" +
				"}";

		TupleQuery tq = conn.prepareTupleQuery(QueryLanguage.SPARQL, query);
		try (TupleQueryResult result = tq.evaluate()) {
			assertNotNull(result);
			while (result.hasNext()) {
				BindingSet bs = result.next();

				IRI p = (IRI) bs.getValue("p");
				assertNotNull(p);
				assertEquals(RDF.TYPE, p);
			}
		}
	}

	private void testSES2052If2(RepositoryConnection conn) throws Exception {
		loadTestData("/testdata-query/dataset-query.trig", conn);
		String query = "SELECT ?p \n" +
				"WHERE { \n" +
				"         ?s ?p ?o . \n" +
				"        FILTER(IF(!BOUND(?p), false , ?p = <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>)) \n" +
				"}";

		TupleQuery tq = conn.prepareTupleQuery(QueryLanguage.SPARQL, query);
		try (TupleQueryResult result = tq.evaluate()) {
			assertNotNull(result);
			while (result.hasNext()) {
				BindingSet bs = result.next();

				IRI p = (IRI) bs.getValue("p");
				assertNotNull(p);
				assertEquals(RDF.TYPE, p);
			}
		}
	}

	private void testRegexCaseNonAscii(RepositoryConnection conn) {

		String query = "ask {filter (regex(\"��������������\", \"��������������\", \"i\")) }";

		assertTrue(conn.prepareBooleanQuery(query).evaluate(), "case-insensitive match on Cyrillic should succeed");

		query = "ask {filter (regex(\"��������������\", \"��������������\")) }";

		assertFalse(conn.prepareBooleanQuery(query).evaluate(), "case-sensitive match on Cyrillic should fail");
	}

	private void testFilterRegexBoolean(RepositoryConnection conn) throws Exception {

		loadTestData("/testdata-query/dataset-query.trig", conn);

		// test case for issue SES-1050
		String query = getNamespaceDeclarations() +
				" SELECT *" +
				" WHERE { " +
				"       ?x foaf:name ?name ; " +
				"          foaf:mbox ?mbox . " +
				"       FILTER(EXISTS { " +
				"            FILTER(REGEX(?name, \"Bo\") && REGEX(?mbox, \"bob\")) " +
				// query.append(" FILTER(REGEX(?mbox, \"bob\")) ");
				"            } )" +
				" } ";

		TupleQuery tq = conn.prepareTupleQuery(QueryLanguage.SPARQL, query);

		try (Stream<BindingSet> result = tq.evaluate().stream()) {
			long count = result.count();
			assertEquals(1, count);
		}
	}

	private void testDateCastFunction_date(RepositoryConnection conn) {
		String qry = "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> "
				+ "SELECT (xsd:date(\"2022-09-09\") AS ?date) { }";

		try (TupleQueryResult result = conn.prepareTupleQuery(QueryLanguage.SPARQL, qry).evaluate()) {
			assertNotNull(result);
			assertTrue(result.hasNext());
			assertEquals("2022-09-09", result.next().getValue("date").stringValue());
			assertFalse(result.hasNext());
		}
	}

	private void testDateCastFunction_date_withTimeZone_utc(RepositoryConnection conn) {
		String qry = "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> "
				+ "SELECT (xsd:date(\"2022-09-09Z\") AS ?date) { }";

		try (TupleQueryResult result = conn.prepareTupleQuery(QueryLanguage.SPARQL, qry).evaluate()) {
			assertNotNull(result);
			assertTrue(result.hasNext());
			assertEquals("2022-09-09Z", result.next().getValue("date").stringValue());
			assertFalse(result.hasNext());
		}
	}

	private void testDateCastFunction_dateTime_withTimeZone_offset(RepositoryConnection conn) {
		String qry = "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> "
				+ "SELECT (xsd:date(\"2022-09-09T14:45:13+03:00\") AS ?date) { }";

		try (TupleQueryResult result = conn.prepareTupleQuery(QueryLanguage.SPARQL, qry).evaluate()) {
			assertNotNull(result);
			assertTrue(result.hasNext());
			assertEquals("2022-09-09+03:00", result.next().getValue("date").stringValue());
			assertFalse(result.hasNext());
		}
	}

	private void testDateCastFunction_invalidInput(RepositoryConnection conn) {
		String qry = "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> "
				+ "SELECT (xsd:date(\"2022-09-xx\") AS ?date) { }";

		try (TupleQueryResult result = conn.prepareTupleQuery(QueryLanguage.SPARQL, qry).evaluate()) {
			assertNotNull(result);
			assertTrue(result.hasNext());
			assertFalse(result.next().hasBinding("date"),
					"There should be no binding because the cast should have failed.");
			assertFalse(result.hasNext());
		}
	}

	public Stream<DynamicTest> tests() {
		return Stream.of(makeTest("Seconds", this::testSeconds),
				makeTest("SecondsMilliseconds", this::testSecondsMilliseconds),
				makeTest("DateCastFunction_invalidInput", this::testDateCastFunction_invalidInput),
				makeTest("DateCastFunction_dateTime_withTimeZone_offset",
						this::testDateCastFunction_dateTime_withTimeZone_offset),
				makeTest("DateCastFunction_date_withTimeZone_utc", this::testDateCastFunction_date_withTimeZone_utc),
				makeTest("DateCastFunction_date", this::testDateCastFunction_date),
				makeTest("FilterRegexBoolean", this::testFilterRegexBoolean),
				makeTest("RegexCaseNonAscii", this::testRegexCaseNonAscii),
				makeTest("SES2052If2", this::testSES2052If2), makeTest("SES2052If1", this::testSES2052If1),
				makeTest("27NormalizeIRIFunction", this::test27NormalizeIRIFunction),
				makeTest("SES2121URIFunction", this::testSES2121URIFunction),
				makeTest("SES1991RANDEvaluation", this::testSES1991RANDEvaluation),
				makeTest("SES1991STRUUIDEvaluation", this::testSES1991STRUUIDEvaluation),
				makeTest("SES1991UUIDEvaluation", this::testSES1991UUIDEvaluation),
				makeTest("SES869ValueOfNow", this::testSES869ValueOfNow),
				makeTest("SES1991NOWEvaluation", this::testSES1991NOWEvaluation));
	}
}