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
@@ -1,19 +1,6 @@
package fr.inria.corese.core.next.query.impl.sparql.bridge;

import fr.inria.corese.core.next.query.impl.sparql.ast.AskQueryAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.ASTConstants;
import fr.inria.corese.core.next.query.impl.sparql.ast.ConstraintAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.DatasetClauseAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.DescribeQueryAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.FilterAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.OrderConditionAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.ProjectionAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.SolutionModifierAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst;
import fr.inria.corese.core.next.query.impl.sparql.ast.*;
import fr.inria.corese.core.next.query.kgram.api.core.ExpType.Type;
import fr.inria.corese.core.next.query.kgram.api.core.Filter;
import fr.inria.corese.core.next.query.kgram.api.core.Node;
Expand All @@ -33,8 +20,9 @@
* Builds KGRAM {@code Exp} / {@code Query} structures from Corese-next query AST nodes.
*
* <p>This bridge sits between the parsed SPARQL AST and the KGRAM runtime query model:
* it does not execute queries, it translates syntax-level query forms ({@code ASK}, {@code SELECT},
* {@code DESCRIBE}) into runtime-ready {@link Query}/{@link Exp} structures.</p>
* it does not execute queries, it translates syntax-level query forms ({@code ASK},
* {@code SELECT}, {@code DESCRIBE}, {@code CONSTRUCT}) into runtime-ready
* {@link Query}/{@link Exp} structures.</p>
*/
public final class CoreseAstQueryBuilder {

Expand Down Expand Up @@ -125,6 +113,32 @@ public Query toNextQuery(DescribeQueryAst describeQueryAst) {
return query;
}

/**
* Builds a KGRAM {@link Query} from a {@link ConstructQueryAst}.
*
* <p>Reuses the shared query shell (compiled {@code WHERE} body, dataset, LIMIT/OFFSET) and
* {@code ORDER BY} like the other forms, then compiles the {@code CONSTRUCT} template into a
* separate {@link Exp} carried by the query. Template variables reuse the runtime node bound by
* the body when visible; a template-only variable stays fresh (an unbound template variable is
* valid SPARQL, its triple is simply skipped at instantiation time). Clauses that require
* dedicated aggregate or values handling are rejected explicitly.</p>
*/
public Query toNextQuery(ConstructQueryAst constructQueryAst) {
Objects.requireNonNull(constructQueryAst, "constructQueryAst");
rejectUnsupportedConstructClauses(constructQueryAst);

Query query = createQuery(
constructQueryAst.whereClause(),
constructQueryAst.datasetClause(),
constructQueryAst.solutionModifier());
applyOrderBy(query, constructQueryAst.solutionModifier());
Exp template = compileConstructTemplate(query, constructQueryAst.constructTemplate());
query.setConstruct(true);
query.setConstruct(template);
query.setConstructNodes(template.getNodes());
return query;
}

/**
* Converts a SPARQL {@code FILTER} clause by converting {@link FilterAst#operator()} the same way as
* {@link #toNextFilter(TermAst)}.
Expand Down Expand Up @@ -423,4 +437,48 @@ private void applyLimitOffset(Query query, SolutionModifierAst solutionModifier)
query.setOffset(Math.toIntExact(solutionModifier.offset()));
}
}

private static void rejectUnsupportedConstructClauses(ConstructQueryAst constructQueryAst) {
if (!constructQueryAst.valuesClause().mappings().isEmpty()) {
throw new UnsupportedOperationException(
"Inline VALUES is not supported yet for CONSTRUCT (values handling is a follow-up)");
}
SolutionModifierAst mod = constructQueryAst.solutionModifier();
if (mod.hasGroupBy() || mod.hasHaving() || mod.distinct() || mod.reduced()) {
throw new UnsupportedOperationException(
"Solution modifiers (GROUP BY, HAVING, DISTINCT, REDUCED) are not supported for CONSTRUCT");
}
// TODO(#389): inline VALUES needs a dedicated runtime mapping.
// TODO(#389): GROUP BY / HAVING would require aggregate-aware CONSTRUCT semantics, not just field copying.
// TODO(#389): DISTINCT / REDUCED are SELECT-only in the grammar; kept as a defensive guard.
}

/**
* Compiles a {@code CONSTRUCT} template into a KGRAM {@link Exp} (a BGP of edges), kept separate
* from the {@code WHERE} body and carried by {@link Query#setConstruct(Exp)}.
*/
private Exp compileConstructTemplate(Query query, ConstructTemplateAst template) {
Exp bgp = Exp.create(Type.BGP);
for (TriplePatternAst triple : template.triplePatternAsts()) {
Node subject = constructNode(query, triple.subject());
Node predicate = constructNode(query, triple.predicate());
Node object = constructNode(query, triple.object());
bgp.add(new AstBackedEdge(subject, predicate, object));
}
return bgp;
}

/**
* Resolves a template term to a runtime {@link Node}. A variable reuses the body node when it is
* bound by the {@code WHERE}; otherwise it stays a fresh node (an unbound template variable is
* valid SPARQL and simply skips its triple at instantiation, so this does not throw). IRIs, blank
* nodes and literals become fresh constant nodes.
*/
private Node constructNode(Query query, TermAst term) {
if (term instanceof VarAst(String name)) {
Node bound = query.getExtNode(name);
return bound != null ? bound : toNode(term);
}
return toNode(term);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package fr.inria.corese.core.next.query.impl.sparql.bridge;

import fr.inria.corese.core.next.query.impl.parser.AbstractSparqlParserFeatureTest;
import fr.inria.corese.core.next.query.impl.parser.SparqlParser;
import fr.inria.corese.core.next.query.impl.sparql.ast.*;
import fr.inria.corese.core.next.query.kgram.api.core.Edge;
import fr.inria.corese.core.next.query.kgram.api.core.Node;
import fr.inria.corese.core.next.query.kgram.core.Exp;
import fr.inria.corese.core.next.query.kgram.core.Query;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.*;

class CoreseAstQueryBuilderConstructTest extends AbstractSparqlParserFeatureTest {

private final CoreseAstQueryBuilder builder = new CoreseAstQueryBuilder();

private static GroupGraphPatternAst whereSPO() {
return new GroupGraphPatternAst(List.of(new BgpAst(List.of(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))))));
}

private static ConstructTemplateAst template(TriplePatternAst... triples) {
return new ConstructTemplateAst(List.of(triples));
}

@Test
@DisplayName("CONSTRUCT { ?x ?p ?o } WHERE { ?x ?p ?o }: flagged CONSTRUCT, template is a BGP with one edge")
void templateCompiledAsBgp() {
ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))), whereSPO());

Query query = builder.toNextQuery(construct);
assertFalse(query.getConstructNodes().isEmpty(), "construct nodes populated");
assertTrue(query.isConstruct(), "query flagged as CONSTRUCT");
assertFalse(query.isSelect(), "CONSTRUCT should not also report itself as SELECT");
assertTrue(query.getBody().isAnd(), "WHERE compiled into the body");
Exp constructExp = query.getConstruct();
assertNotNull(constructExp, "template stored via setConstruct(Exp)");
assertTrue(constructExp.isBGP(), "template is a BGP");
assertEquals(1, constructExp.size(), "one template triple -> one edge");
}

@Test
@DisplayName("Parser -> ConstructQueryAst -> Query keeps the CONSTRUCT form")
void buildsQueryFromParsedConstruct() {
SparqlParser parser = newParserDefault();
ConstructQueryAst construct = assertInstanceOf(ConstructQueryAst.class, parser.parse("CONSTRUCT { ?x ?p ?o } WHERE { ?x ?p ?o }"));

Query query = builder.toNextQuery(construct);

assertTrue(query.isConstruct());
assertFalse(query.isSelect());
assertTrue(query.getBody().get(0).isBGP());
assertNotNull(query.getConstruct());
assertEquals(1, query.getConstruct().size());
}

@Test
@DisplayName("Two template triples produce two edges")
void templateWithTwoTriples() {
ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new IriAst("http://example.org/p"), new VarAst("o")), new TriplePatternAst(new VarAst("x"), new IriAst("http://example.org/q"), new VarAst("o"))), whereSPO());

Query query = builder.toNextQuery(construct);

assertEquals(2, query.getConstruct().size());
}

@Test
@DisplayName("Template variable bound by WHERE reuses the runtime body node")
void templateVariableResolvesToBodyNode() {
ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))), whereSPO());

Query query = builder.toNextQuery(construct);

Edge edge = query.getConstruct().first().getEdge();
assertSame(query.getExtNode("x"), edge.getNode(0), "template ?x is the body node");
assertSame(query.getExtNode("o"), edge.getNode(1), "template ?o is the body node");
}

@Test
@DisplayName("Template-only variable (not in WHERE) stays fresh and does not throw")
void templateOnlyVariableStaysFresh() {
ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("newVar"))), whereSPO());

Query query = assertDoesNotThrow(() -> builder.toNextQuery(construct));

assertNull(query.getExtNode("newVar"), "template-only variable is not injected into the body");
Node object = query.getConstruct().first().getEdge().getNode(1);
assertTrue(object.isVariable(), "template-only variable is still a variable node");
}

@Test
@DisplayName("Template IRIs become fixed (non-variable) nodes")
void templateWithFixedResources() {
ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new IriAst("http://example.org/s"), new IriAst("http://example.org/p"), new VarAst("o"))), whereSPO());

Query query = builder.toNextQuery(construct);

Edge edge = query.getConstruct().first().getEdge();
assertFalse(edge.getNode(0).isVariable(), "fixed subject IRI");
assertFalse(edge.getProperty().isVariable(), "fixed predicate IRI");
assertTrue(edge.getNode(1).isVariable(), "object still bound to body ?o");
}

@Test
@DisplayName("FROM / FROM NAMED, LIMIT and OFFSET are applied")
void appliesDatasetLimitOffset() {
DatasetClauseAst dataset = new DatasetClauseAst(Set.of(new IriAst("http://example.org/g")), Set.of(new IriAst("http://example.org/n")));
SolutionModifierAst mod = SolutionModifierAst.withoutGroupBy(false, false, List.of(), 5L, 2L);
ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))), dataset, whereSPO(), mod);

Query query = builder.toNextQuery(construct);

assertEquals(List.of("http://example.org/g"), query.getFrom().stream().map(Node::getLabel).toList(), "FROM applied");
assertEquals(List.of("http://example.org/n"), query.getNamed().stream().map(Node::getLabel).toList(), "FROM NAMED applied");
assertEquals(5, query.getLimit(), "LIMIT applied");
assertEquals(2, query.getOffset(), "OFFSET applied");
}

@Test
@DisplayName("ORDER BY ?x is applied")
void appliesOrderBy() {
SolutionModifierAst mod = SolutionModifierAst.withoutGroupBy(false, false, List.of(new OrderConditionAst(ASTConstants.OrderDirection.ASC, new VarAst("x"))), null, null);
ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))), DatasetClauseAst.none(), whereSPO(), mod);

Query query = builder.toNextQuery(construct);

assertFalse(query.getOrderBy().isEmpty(), "ORDER BY applied");
}

@Test
@DisplayName("Inline VALUES is not supported yet -> UnsupportedOperationException")
void rejectsValuesClause() {
ValuesAst values = new ValuesAst(List.of(new ValueMappingAst(Map.of(new VarAst("x"), new IriAst("http://example.org/v")))));
ConstructQueryAst construct = new ConstructQueryAst(template(new TriplePatternAst(new VarAst("x"), new VarAst("p"), new VarAst("o"))), DatasetClauseAst.none(), whereSPO(), null, null, values);

assertThrows(UnsupportedOperationException.class, () -> builder.toNextQuery(construct));
}

@Test
@DisplayName("toNextQuery((ConstructQueryAst) null) throws NullPointerException")
void rejectsNull() {
assertThrows(NullPointerException.class, () -> builder.toNextQuery((ConstructQueryAst) null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ private static void assertLoweredDescribeConstruct(Query query, int describedNod
assertNotNull(query.getConstruct(), "construct template created");
assertEquals(2 * describedNodeCount, query.getConstruct().size(),
"construct template has outgoing and incoming triple patterns per described node");
assertNotNull(query.getConstructNodes(), "construct nodes collected");
assertFalse(query.getConstructNodes().isEmpty(), "construct nodes collected");
}

/**
Expand Down
Loading