TupleExprBuilderTest.java

/*******************************************************************************
 * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
 *
 * 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;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.query.algebra.Extension;
import org.eclipse.rdf4j.query.algebra.Order;
import org.eclipse.rdf4j.query.algebra.Projection;
import org.eclipse.rdf4j.query.algebra.Service;
import org.eclipse.rdf4j.query.algebra.SingletonSet;
import org.eclipse.rdf4j.query.algebra.Slice;
import org.eclipse.rdf4j.query.algebra.TupleExpr;
import org.eclipse.rdf4j.query.algebra.Var;
import org.eclipse.rdf4j.query.parser.sparql.ast.ASTQueryContainer;
import org.eclipse.rdf4j.query.parser.sparql.ast.ASTServiceGraphPattern;
import org.eclipse.rdf4j.query.parser.sparql.ast.ASTUpdateSequence;
import org.eclipse.rdf4j.query.parser.sparql.ast.ParseException;
import org.eclipse.rdf4j.query.parser.sparql.ast.SyntaxTreeBuilder;
import org.eclipse.rdf4j.query.parser.sparql.ast.TokenMgrError;
import org.eclipse.rdf4j.query.parser.sparql.ast.VisitorException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;

/**
 * @author jeen
 */
public class TupleExprBuilderTest {

	private TupleExprBuilder builder;

	@BeforeEach
	public void setupBuilder() {
		builder = new TupleExprBuilder(SimpleValueFactory.getInstance());
	}

	@Test
	public void testSimpleAliasHandling() {
		String query = "SELECT (?a as ?b) WHERE { ?a ?x ?z }";

		try {
			ASTQueryContainer qc = SyntaxTreeBuilder.parseQuery(query);
			TupleExpr result = builder.visit(qc, null);

			assertThat(result instanceof Projection).isTrue();

			Projection p = (Projection) result;
			assertThat(p.getArg()).isInstanceOf(Extension.class);
			Extension extension = (Extension) p.getArg();

			assertThat(extension.getElements()).hasSize(1)
					.allMatch(elem -> elem.getName().equals("b") && ((Var) elem.getExpr()).getName().equals("a"));

		} catch (Exception e) {
			e.printStackTrace();
			fail("should parse simple select query");
		}
	}

	@Test
	public void testBindVarReuseHandling() {
		String query = "SELECT * WHERE { ?s ?p ?o. BIND(<foo:bar> as ?o) }";

		assertThatExceptionOfType(VisitorException.class).isThrownBy(() -> {
			ASTQueryContainer qc = SyntaxTreeBuilder.parseQuery(query);
			builder.visit(qc, null);
		}).withMessageContaining("BIND clause alias 'o' was previously used");
	}

	@Test
	public void testBindVarReuseHandling2() {
		String query = "SELECT * WHERE { { ?s ?p ?o } BIND(<foo:bar> as ?o) }";

		assertThatExceptionOfType(VisitorException.class).isThrownBy(() -> {
			ASTQueryContainer qc = SyntaxTreeBuilder.parseQuery(query);
			builder.visit(qc, null);
		}).withMessageContaining("BIND clause alias 'o' was previously used");
	}

	@Test
	public void testBindVarReuseHandling3() {
		String query = "SELECT * WHERE {  BIND(<foo:bar> as ?o) ?s ?p ?o. }";

		ASTQueryContainer qc;
		try {
			qc = SyntaxTreeBuilder.parseQuery(query);
			builder.visit(qc, null);
		} catch (Exception e) {
			fail("BIND alias before reuse in BGP should be allowed");
		}
	}

	@Test
	public void testAskQuerySolutionModifiers() {
		String query = "ASK WHERE { ?foo ?bar ?baz . } ORDER BY ?foo LIMIT 1";

		try {
			ASTQueryContainer qc = SyntaxTreeBuilder.parseQuery(query);
			TupleExpr result = builder.visit(qc, null);
			assertTrue(result instanceof Order);
		} catch (Exception e) {
			e.printStackTrace();
			fail("should parse ask query with solution modifiers");
		}

	}

	@Test
	public void testNegatedPathWithFixedObject() {
		String query = "ASK WHERE { ?s !<http://example.org/p> <http://example.org/o> . }";

		try {
			ASTQueryContainer qc = SyntaxTreeBuilder.parseQuery(query);
			TupleExpr result = builder.visit(qc, null);

			assertTrue(result instanceof Slice);
		} catch (Exception e) {
			e.printStackTrace();
			fail("should parse ask query with negated property path");
		}
	}

	/**
	 * Verifies that a missing close brace does not cause an endless loop. Timeout is set to avoid test itself endlessly
	 * looping. See SES-2389.
	 */
	@Test
	@Timeout(10)
	public void testMissingCloseBrace() {
		String query = "INSERT DATA { <urn:a> <urn:b> <urn:c> .";
		try {
			final ASTUpdateSequence us = SyntaxTreeBuilder.parseUpdateSequence(query);
			fail("should result in parse error");
		} catch (ParseException e) {
			// fall through, expected
		}
	}

	@Test
	public void testServiceGraphPatternStringDetection1() throws TokenMgrError, ParseException, VisitorException {

		String servicePattern = "SERVICE <foo:bar> { ?x <foo:baz> ?y }";

		StringBuilder qb = new StringBuilder();
		qb.append("SELECT * \n");
		qb.append("WHERE { \n");
		qb.append(" { ?s ?p ?o } \n");
		qb.append(" UNION \n");
		qb.append(" { ?p ?q ?r } \n");
		qb.append(servicePattern);
		qb.append("\n");
		qb.append(" FILTER (?s = <foo:bar>) ");
		qb.append(" } ");

		ASTQueryContainer qc = SyntaxTreeBuilder.parseQuery(qb.toString());

		ServiceNodeFinder f = new ServiceNodeFinder();
		f.visit(qc, null);

		assertTrue(f.getGraphPatterns().size() == 1);
		assertTrue(servicePattern.equals(f.getGraphPatterns().get(0)));
	}

	@Test
	public void testServiceGraphPatternStringDetection2() throws TokenMgrError, ParseException, VisitorException {

		String servicePattern = "SERVICE <foo:bar> \r\n { ?x <foo:baz> ?y. \r\n \r\n }";

		StringBuilder qb = new StringBuilder();
		qb.append("SELECT * \n");
		qb.append("WHERE { \n");
		qb.append(" { ?s ?p ?o } \n");
		qb.append(" UNION \n");
		qb.append(" { ?p ?q ?r } \n");
		qb.append(servicePattern);
		qb.append("\n");
		qb.append(" FILTER (?s = <foo:bar>) ");
		qb.append(" } ");

		ASTQueryContainer qc = SyntaxTreeBuilder.parseQuery(qb.toString());

		ServiceNodeFinder f = new ServiceNodeFinder();
		f.visit(qc, null);

		assertTrue(f.getGraphPatterns().size() == 1);
		assertTrue(servicePattern.equals(f.getGraphPatterns().get(0)));
	}

	@Test
	public void testServiceGraphPatternStringDetection3() throws TokenMgrError, ParseException, VisitorException {

		String servicePattern1 = "SERVICE <foo:bar> \n { ?x <foo:baz> ?y. }";
		String servicePattern2 = "SERVICE <foo:bar2> \n { ?x <foo:baz> ?y. }";

		StringBuilder qb = new StringBuilder();
		qb.append("SELECT * \n");
		qb.append("WHERE { \n");
		qb.append(servicePattern1);
		qb.append(" OPTIONAL { \n");
		qb.append(servicePattern2);
		qb.append("    } \n");
		qb.append(" } ");

		ASTQueryContainer qc = SyntaxTreeBuilder.parseQuery(qb.toString());

		ServiceNodeFinder f = new ServiceNodeFinder();
		f.visit(qc, null);

		assertTrue(f.getGraphPatterns().size() == 2);
		assertTrue(servicePattern1.equals(f.getGraphPatterns().get(0)));
		assertTrue(servicePattern2.equals(f.getGraphPatterns().get(1)));
	}

	@Test
	public void testServiceGraphPatternStringDetection4() throws TokenMgrError, ParseException, VisitorException {

		String servicePattern1 = "SERVICE <http://localhost:18080/openrdf/repositories/endpoint1> {  ?s ?p ?o1 . "
				+ "OPTIONAL {	SERVICE SILENT <http://invalid.endpoint.org/sparql> { ?s ?p2 ?o2 } } }";

		String servicePattern2 = "SERVICE SILENT <http://invalid.endpoint.org/sparql> { ?s ?p2 ?o2 }";

		StringBuilder qb = new StringBuilder();
		qb.append("SELECT * \n");
		qb.append("WHERE { \n");
		qb.append(servicePattern1);
		qb.append(" } ");

		ASTQueryContainer qc = SyntaxTreeBuilder.parseQuery(qb.toString());

		ServiceNodeFinder f = new ServiceNodeFinder();
		f.visit(qc, null);

		assertTrue(f.getGraphPatterns().size() == 2);
		assertTrue(servicePattern1.equals(f.getGraphPatterns().get(0)));
		assertTrue(servicePattern2.equals(f.getGraphPatterns().get(1)));
	}

	@Test
	public void testServiceGraphPatternChopping() {

		// just for construction
		Service service = new Service(new Var(null, null, false, false), new SingletonSet(), "", null, null, false);

		service.setExpressionString("SERVICE <a> { ?s ?p ?o }");
		assertEquals("?s ?p ?o", service.getServiceExpressionString());

		service.setExpressionString("SERVICE <a> {?s ?p ?o}");
		assertEquals("?s ?p ?o", service.getServiceExpressionString());

	}

	@Test
	public void testOtionalBindCoalesce() throws Exception {
		StringBuilder qb = new StringBuilder();
		qb.append("SELECT ?result \n");
		qb.append("WHERE { \n");
		qb.append("OPTIONAL {\n" +
				"        OPTIONAL {\n" +
				"            BIND(\"value\" AS ?foo)\n" +
				"        }\n" +
				"        BIND(COALESCE(?foo, \"no value\") AS ?result)\n" +
				"    }");
		qb.append(" } ");

		ASTQueryContainer qc = SyntaxTreeBuilder.parseQuery(qb.toString());
		TupleExpr result = builder.visit(qc, null);
		String expected = "Projection\n" +
				"   ProjectionElemList\n" +
				"      ProjectionElem \"result\"\n" +
				"   LeftJoin\n" +
				"      SingletonSet\n" +
				"      Extension\n" +
				"         LeftJoin\n" +
				"            SingletonSet\n" +
				"            Extension\n" +
				"               SingletonSet\n" +
				"               ExtensionElem (foo)\n" +
				"                  ValueConstant (value=\"value\")\n" +
				"         ExtensionElem (result)\n" +
				"            Coalesce\n" +
				"               Var (name=foo)\n" +
				"               ValueConstant (value=\"no value\")\n";
		assertEquals(expected.replace("\r\n", "\n"), result.toString().replace("\r\n", "\n"));
//		System.out.println(result);
	}

	private class ServiceNodeFinder extends AbstractASTVisitor {

		private final List<String> graphPatterns = new ArrayList<>();

		@Override
		public Object visit(ASTServiceGraphPattern node, Object data) throws VisitorException {
			graphPatterns.add(node.getPatternString());
			return super.visit(node, data);
		}

		public List<String> getGraphPatterns() {
			return graphPatterns;
		}
	}
}