From 44cf4cf44ffb1eb476cb1ad9bc3d56d2bd8eb0ec Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Mon, 27 Jul 2026 10:40:59 +0200 Subject: [PATCH 1/3] [Feature] Progressively refactor SparqlAstBuilder --- .../query/impl/parser/SparqlAstBuilder.java | 871 +----------------- .../impl/parser/SparqlExpressionBuilder.java | 635 +++++++++++++ .../query/impl/parser/SparqlPathBuilder.java | 151 +++ .../query/impl/parser/SparqlTermBuilder.java | 260 ++++++ .../parser/SparqlExpressionBuilderTest.java | 286 ++++++ .../impl/parser/SparqlPathBuilderTest.java | 182 ++++ .../impl/parser/SparqlTermBuilderTest.java | 207 +++++ 7 files changed, 1763 insertions(+), 829 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlExpressionBuilder.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlPathBuilder.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlTermBuilder.java create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlExpressionBuilderTest.java create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlPathBuilderTest.java create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlTermBuilderTest.java diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java index 397f50d05..404a9af25 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java @@ -1,18 +1,14 @@ package fr.inria.corese.core.next.query.impl.parser; -import fr.inria.corese.core.next.data.impl.io.common.IOConstants; import fr.inria.corese.core.next.data.impl.common.vocabulary.RDF; -import fr.inria.corese.core.next.data.impl.common.vocabulary.XSD; +import fr.inria.corese.core.next.data.impl.io.common.IOConstants; import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser; import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; import fr.inria.corese.core.next.query.api.exception.QueryValidationException; import fr.inria.corese.core.next.query.impl.parser.semantic.support.VariableScopeAnalyzer; import fr.inria.corese.core.next.query.impl.sparql.ast.*; -import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; import fr.inria.corese.core.next.query.impl.sparql.ast.path.*; -import org.antlr.v4.runtime.tree.ParseTree; -import org.antlr.v4.runtime.tree.TerminalNode; import java.util.*; @@ -145,9 +141,23 @@ protected record ServiceEntry(int groupDepth, TermAst endpoint, boolean silent) */ protected final Deque capturedExistsStack = new ArrayDeque<>(); + /** + * Stateless term-building helpers. + */ + protected final SparqlTermBuilder terms; + + /** Property-path building helpers. */ + protected final SparqlPathBuilder paths; + + /** Expression and constraint building helpers. */ + protected final SparqlExpressionBuilder expressions; + protected SparqlAstBuilder(SparqlParserOptions options) { this.options = options; this.baseUri = options.getBaseIRI(); + this.terms = new SparqlTermBuilder(options, this::termFromBlankNode); + this.paths = new SparqlPathBuilder(this.terms); + this.expressions = new SparqlExpressionBuilder(this.terms, this::popCapturedExistsPattern); } public void reserveBlankNodeLabels(Collection labels) { @@ -501,7 +511,6 @@ public void enterNotExistsFunc() { /** * Pops the captured GroupGraphPattern from the last closed EXISTS/NOT EXISTS block. - * Called by {@link #termFromBuiltInCall} after the listener has closed the group. */ public GroupGraphPatternAst popCapturedExistsPattern() { return capturedExistsStack.pollFirst(); @@ -514,12 +523,7 @@ public GroupGraphPatternAst popCapturedExistsPattern() { * Variable token text can be "?s" or "$s" depending on grammar. */ public TermAst var(String tokenText) { - if (tokenText == null || tokenText.isBlank()) { - throw new IllegalArgumentException("Variable token text is null/blank"); - } - String t = tokenText.trim(); - if (t.startsWith("?") || t.startsWith("$")) t = t.substring(1); - return new VarAst(t); + return terms.var(tokenText); } /** @@ -531,9 +535,7 @@ public TermAst var(String tokenText) { * */ public TermAst iri(String raw) { - if (raw == null) throw new IllegalArgumentException("IRI raw is null"); - if(raw.equals("a")) return new IriAst("<" + RDF.type.getIRI().stringValue() + ">"); - return new IriAst(raw); + return terms.iri(raw); } /** @@ -543,204 +545,31 @@ public TermAst iri(String raw) { * - datatype as IRI/QName text (e.g., "xsd:integer"), or null */ public TermAst literal(String lexical, String lang, String datatype) { - if (lexical == null) throw new IllegalArgumentException("Literal lexical is null"); - return new LiteralAst(lexical, lang, datatype); + return terms.literal(lexical, lang, datatype); } - /** - * Creates the right createFunCall AST according to keyword and argument list - * - * @param constraint keyword - * @param args arguments list - * @return An ConstraintAst - */ - public ConstraintAst createConstraint(ASTConstants.Constraint constraint, List args) { - switch (constraint) { - case ASTConstants.OPERATOR.NOT -> { - return new BooleanNotAst(args); - } - case ASTConstants.OPERATOR.PLUS -> { - return new UnaryPlusAst(args); - } - case ASTConstants.OPERATOR.MINUS -> { - return new UnaryMinusAst(args); - } - case ASTConstants.FUNCTION_CALL.BOUND -> { - return new BoundAst(args); - } - case ASTConstants.FUNCTION_CALL.IS_IRI -> { - return new IsIriAst(args); - } - case ASTConstants.FUNCTION_CALL.IS_BLANK -> { - return new IsBlankAst(args); - } - case ASTConstants.FUNCTION_CALL.IS_LITERAL -> { - return new IsLiteralAst(args); - } - case ASTConstants.FUNCTION_CALL.STR -> { - return new StrAst(args); - } - case ASTConstants.FUNCTION_CALL.UCASE -> { - return new UcaseAst(args); - } - case ASTConstants.FUNCTION_CALL.LCASE -> { - return new LcaseAst(args); - } - case ASTConstants.FUNCTION_CALL.LANG -> { - return new LangAst(args); - } - case ASTConstants.FUNCTION_CALL.STRDT -> { - return new StrDtAst(args); - } - case ASTConstants.FUNCTION_CALL.STRLANG -> { - return new StrLangAst(args); - } - case ASTConstants.FUNCTION_CALL.DATATYPE -> { - return new DatatypeAst(args); - } - case ASTConstants.FUNCTION_CALL.IRI -> { - return new IriFunctionAst(args); - } - case ASTConstants.FUNCTION_CALL.BNODE -> { - return new BnodeAst(args); - } - case ASTConstants.OPERATOR.OR -> { - return new OrAst(args); - } - case ASTConstants.OPERATOR.AND -> { - return new AndAst(args); - } - case ASTConstants.OPERATOR.EQ -> { - return new EqualsAst(args); - } - case ASTConstants.OPERATOR.NE -> { - return new DifferentAst(args); - } - case ASTConstants.OPERATOR.LT -> { - return new LowerThanAst(args); - } - case ASTConstants.OPERATOR.LE -> { - return new LowerOrEqualThanAst(args); - } - case ASTConstants.OPERATOR.GT -> { - return new GreaterThanAst(args); - } - case ASTConstants.OPERATOR.GE -> { - return new GreaterOrEqualThanAst(args); - } - case ASTConstants.OPERATOR.MUL -> { - return new MultiplyAst(args); - } - case ASTConstants.OPERATOR.DIV -> { - return new DivideAst(args); - } - case ASTConstants.OPERATOR.ADD -> { - return new AddAst(args); - } - case ASTConstants.OPERATOR.SUB -> { - return new SubtractAst(args); - } - case ASTConstants.FUNCTION_CALL.SAMETERM -> { - return new SameTermAst(args); - } - case ASTConstants.FUNCTION_CALL.LANGMATCHES -> { - return new LangMatchesAst(args); - } - case ASTConstants.FUNCTION_CALL.CONTAINS -> { - return new ContainsAst(args); - } - case ASTConstants.FUNCTION_CALL.STRSTARTS -> { - return new StrStartsAst(args); - } - case ASTConstants.FUNCTION_CALL.STRENDS -> { - return new StrEndsAst(args); - } - case ASTConstants.FUNCTION_CALL.SUBSTR -> { - return new SubstrAst(args); - } - case ASTConstants.FUNCTION_CALL.CONCAT -> { - return new ConcatAst(args); - } - case ASTConstants.FUNCTION_CALL.STRBEFORE -> { - return new StrBeforeAst(args); - } - case ASTConstants.FUNCTION_CALL.STRAFTER -> { - return new StrAfterAst(args); - } - case ASTConstants.FUNCTION_CALL.REPLACE -> { - return new ReplaceAst(args); - } - case ASTConstants.FUNCTION_CALL.REGEX -> { - if (args.size() == 2) { - return new BinaryRegexAst(args); - } else if (args.size() == 3) { - return new TrinaryRegexAst(args); - } else { - throw new QueryEvaluationException("Unexpected number of arguments (3) for REGEX keyword"); - } - } - default -> - throw new QueryEvaluationException("Unexpected number of arguments (" + args.size() + ") for " + constraint); - } - } - - public ConstraintAst createFunCall(IriAst functionName, List args) { - return new FunctionCallAst(functionName, args); - } - - public AggregateAst createAggregate( - AggregateFunction function, boolean distinct, TermAst expression, String groupConcatSeparator) { - return new AggregateAst(function, distinct, expression, groupConcatSeparator); - } // ---- term helpers ---- public TermAst termFromVerb(SparqlParser.VerbContext ctx) { - if (ctx.A() != null) return this.iri("a"); - return termFromVarOrIriRef(ctx.varOrIri()); + return terms.termFromVerb(ctx); } public TermAst termFromVarOrTerm(SparqlParser.VarOrTermContext ctx) { - if (ctx.var_() != null) return termFromVar(ctx.var_()); - return termFromGraphTerm(ctx.graphTerm()); + return terms.termFromVarOrTerm(ctx); } public TermAst termFromVarOrIriRef(SparqlParser.VarOrIriContext ctx) { - if (ctx.var_() != null) { - return termFromVar(ctx.var_()); - } - return termFromIriRef(ctx.iriRef()); + return terms.termFromVarOrIriRef(ctx); } public TermAst termFromGraphTerm(SparqlParser.GraphTermContext ctx) { - if (ctx.iriRef() != null) { - return termFromIriRef(ctx.iriRef()); - } - if (ctx.rdfLiteral() != null) { - return termFromRdfLiteral(ctx.rdfLiteral()); - } - if (ctx.numericLiteral() != null) { - return termFromNumericLiteral(ctx.numericLiteral()); - } - if (ctx.booleanLiteral() != null) { - return termFromBooleanLiteral(ctx.booleanLiteral()); - } - if (ctx.blankNode() != null) { - return termFromBlankNode(ctx.blankNode()); - } - if (ctx.NIL() != null) { - return this.iri("()"); - } // NIL = () in SPARQL - return this.iri(ctx.getText()); + return terms.termFromGraphTerm(ctx); } public List termListFromObjectList(SparqlParser.ObjectListContext ctx) { - List out = new ArrayList<>(); - for (var obj : ctx.object_()) { - out.add(termFromObject(obj)); - } - return out; + return terms.termListFromObjectList(ctx, this::termFromObject); } public TermAst termFromObject(SparqlParser.Object_Context ctx) { @@ -759,121 +588,23 @@ public TermAst termFromGraphNode(SparqlParser.GraphNodeContext ctx) { } public TermAst termFromRdfLiteral(SparqlParser.RdfLiteralContext ctx) { - // rdfLiteral : string_ ( LANGTAG | '^^' iriRef )? - - String lexical = ctx.string_().getText(); - String lang = null; - String datatype = null; - - if (ctx.LANGTAG() != null) { - String t = ctx.LANGTAG().getText(); // ex: "@fr" - lang = t.startsWith("@") ? t.substring(1) : t; - } else if (ctx.DOUBLE_CARET() != null && ctx.iriRef() != null) { - datatype = ctx.iriRef().getText(); // ex: xsd:integer ou - } - return this.literal(lexical, lang, datatype); - } - - public TermAst termFromPrimary(SparqlParser.PrimaryExpressionContext ctx) { - if (ctx.brackettedExpression() != null) { - return termFromBrackettedExpression(ctx.brackettedExpression()); - } else if (ctx.builtInCall() != null) { - return termFromBuiltInCall(ctx.builtInCall()); - } else if (ctx.iriRefOrFunction() != null) { - return termFromIriRefOrFunction(ctx.iriRefOrFunction()); - } else if (ctx.rdfLiteral() != null) { - return termFromRdfLiteral(ctx.rdfLiteral()); - } else if (ctx.numericLiteral() != null) { - return termFromNumericLiteral(ctx.numericLiteral()); - } else if (ctx.booleanLiteral() != null) { - return termFromBooleanLiteral(ctx.booleanLiteral()); - } else if (ctx.var_() != null) { - return termFromVar(ctx.var_()); - } else { - throw new QueryEvaluationException("Unexpected content of bracketed termFromExpression"); - } + return terms.termFromRdfLiteral(ctx); } public TermAst termFromVar(SparqlParser.Var_Context ctx) { - return this.var(ctx.getText()); + return terms.termFromVar(ctx); } public TermAst termFromBooleanLiteral(SparqlParser.BooleanLiteralContext ctx) { - if (ctx.FALSE() != null) { - return new LiteralAst("false", null, XSD.xsdBoolean.getIRI().stringValue()); - } else if (ctx.TRUE() != null) { - return new LiteralAst("true", null, XSD.xsdBoolean.getIRI().stringValue()); - } else { - throw new QueryEvaluationException("Unexpected value for boolean literal"); - } - } - - public TermAst termFromIriRefOrFunction(SparqlParser.IriRefOrFunctionContext ctx) { - if (ctx.iriRef() != null && ctx.argList() == null) { - return termFromIriRef(ctx.iriRef()); - } else if (ctx.iriRef() != null && ctx.argList() != null) { - List args = termListFromArgList(ctx.argList()); - IriAst iriRef = (IriAst) termFromIriRef(ctx.iriRef()); - return createFunCall(iriRef, args); - } else { - throw new QueryEvaluationException("Unexpected element in IRI ref or function"); - } - } - - public List termListFromArgList(SparqlParser.ArgListContext ctx) { - return ctx.expression().stream().map(this::termFromExpression).toList(); + return terms.termFromBooleanLiteral(ctx); } public TermAst termFromIriRef(SparqlParser.IriRefContext ctx) { - return this.iri(ctx.getText()); + return terms.termFromIriRef(ctx); } public TermAst termFromNumericLiteral(SparqlParser.NumericLiteralContext ctx) { - if (ctx.numericLiteralUnsigned() != null) { - return termFromNumericLiteralUnsigned(ctx.numericLiteralUnsigned()); - } else if (ctx.numericLiteralPositive() != null) { - return termFromNumericLiteralPositive(ctx.numericLiteralPositive()); - } else if (ctx.numericLiteralNegative() != null) { - return termFromNumericLiteralNegative(ctx.numericLiteralNegative()); - } else { - throw new QueryEvaluationException("Unexpected content for numeric literal"); - } - } - - public TermAst termFromNumericLiteralNegative(SparqlParser.NumericLiteralNegativeContext ctx) { - if (ctx.INTEGER_NEGATIVE() != null) { - return this.literal(ctx.getText(), null, XSD.xsdNegativeInteger.getIRI().stringValue()); - } else if (ctx.DECIMAL_NEGATIVE() != null) { - return this.literal(ctx.getText(), null, XSD.xsdDecimal.getIRI().stringValue()); - } else if (ctx.DOUBLE_NEGATIVE() != null) { - return this.literal(ctx.getText(), null, XSD.xsdDouble.getIRI().stringValue()); - } else { - throw new QueryEvaluationException("Unexpected content for negative numeric literal"); - } - } - - public TermAst termFromNumericLiteralPositive(SparqlParser.NumericLiteralPositiveContext ctx) { - if (ctx.INTEGER_POSITIVE() != null) { - return this.literal(ctx.getText(), null, XSD.xsdPositiveInteger.getIRI().stringValue()); - } else if (ctx.DECIMAL_POSITIVE() != null) { - return this.literal(ctx.getText(), null, XSD.xsdDecimal.getIRI().stringValue()); - } else if (ctx.DOUBLE_POSITIVE() != null) { - return this.literal(ctx.getText(), null, XSD.xsdDouble.getIRI().stringValue()); - } else { - throw new QueryEvaluationException("Unexpected content for positive numeric literal"); - } - } - - public TermAst termFromNumericLiteralUnsigned(SparqlParser.NumericLiteralUnsignedContext ctx) { - if (ctx.INTEGER() != null) { - return this.literal(ctx.getText(), null, XSD.xsdUnsignedInt.getIRI().stringValue()); - } else if (ctx.DECIMAL() != null) { - return this.literal(ctx.getText(), null, XSD.xsdDecimal.getIRI().stringValue()); - } else if (ctx.DOUBLE() != null) { - return this.literal(ctx.getText(), null, XSD.xsdDouble.getIRI().stringValue()); - } else { - throw new QueryEvaluationException("Unexpected content for positive numeric literal"); - } + return terms.termFromNumericLiteral(ctx); } public TermAst termFromBlankNode(SparqlParser.BlankNodeContext ctx) { @@ -883,512 +614,31 @@ public TermAst termFromBlankNode(SparqlParser.BlankNodeContext ctx) { } /** - * One {@code groupCondition} : {@code builtInCall | functionCall | '(' expression ('AS' var_)? ')' | var_}. + * One {@code groupCondition} : {@code builtInCall | functionCall | ‘(‘ expression (‘AS’ var_)? ‘)’ | var_}. *

La forme {@code (expr AS ?v)} est résolue comme l’expression de regroupement (le nom optionnel rejoint la * spec en étant porté par la projection SELECT ; l’AST minimal ne le duplique pas ici). */ public TermAst termFromGroupCondition(SparqlParser.GroupConditionContext ctx) { - if (ctx.expression() != null) { - return termFromExpression(ctx.expression()); - } - if (ctx.var_() != null) { - return termFromVar(ctx.var_()); - } - if (ctx.builtInCall() != null) { - return termFromBuiltInCall(ctx.builtInCall()); - } - if (ctx.functionCall() != null) { - SparqlParser.FunctionCallContext fc = ctx.functionCall(); - IriAst functionName = new IriAst(fc.iriRef().getText()); - if (fc.argList() == null) { - return createFunCall(functionName, List.of()); - } - if (fc.argList().NIL() != null) { - return createFunCall(functionName, List.of()); - } - List args = fc.argList().expression().stream().map(this::termFromExpression).toList(); - return createFunCall(functionName, args); - } - throw new QueryEvaluationException("Unsupported group condition: " + ctx.getText()); + return expressions.termFromGroupCondition(ctx); } public TermAst termFromConstraint(SparqlParser.ConstraintContext ctx) { - if (ctx.builtInCall() != null) { - return termFromBuiltInCall(ctx.builtInCall()); - } else if (ctx.functionCall() != null) { - IriAst functionTermAst = new IriAst(ctx.functionCall().iriRef().getText()); - List args = ctx.functionCall().argList().expression().stream().map(this::termFromExpression).toList(); - return new FunctionCallAst(functionTermAst, args); - } else if (ctx.brackettedExpression() != null && ctx.brackettedExpression().expression() != null) { - return termFromBrackettedExpression(ctx.brackettedExpression()); - } else { - throw new QueryEvaluationException("No createFunCall found in filter"); - } - } - - public TermAst termFromBuiltInCall(SparqlParser.BuiltInCallContext ctx) { - if (ctx.aggregate() != null) { - return termFromAggregate(ctx.aggregate()); - } - if (ctx.existsFunc() != null) { - GroupGraphPatternAst existsPattern = popCapturedExistsPattern(); - if (existsPattern == null) { - throw new QueryEvaluationException("EXISTS { ... } inner pattern was not captured; check listener order"); - } - return new ExistsAst(existsPattern); - } - if (ctx.notExistsFunc() != null) { - GroupGraphPatternAst notExistsPattern = popCapturedExistsPattern(); - if (notExistsPattern == null) { - throw new QueryEvaluationException("NOT EXISTS { ... } inner pattern was not captured; check listener order"); - } - return new NotExistsAst(notExistsPattern); - } - if (ctx.regexExpression() != null) { - return termFromRegex(ctx.regexExpression()); - } else if (ctx.strReplaceExpression() != null) { - return termFromReplace(ctx.strReplaceExpression()); - } else if (ctx.BOUND() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.BOUND, List.of(this.var(ctx.var_().getText()))); - } else if (ctx.BNODE() != null) { - List args = ctx.expression() == null - ? List.of() - : ctx.expression().stream().map(this::termFromExpression).toList(); - return this.createConstraint(ASTConstants.FUNCTION_CALL.BNODE, args); - } else if (ctx.IF() != null) { - List args = ctx.expression().stream().map(this::termFromExpression).toList(); - return new IfAst(args.get(0), args.get(1), args.get(2)); - } else if (ctx.RAND() != null) { - return new RandAst(); - } else if (ctx.UUID() != null) { - return new UuidAst(); - } else if (ctx.STRUUID() != null) { - return new StrUuidAst(); - } else if (ctx.CONCAT() != null) { - List args = ctx.expression().stream().map(this::termFromExpression).toList(); - return this.createConstraint(ASTConstants.FUNCTION_CALL.CONCAT, args); - } else if (ctx.COALESCE() != null) { - List args = ctx.expression().stream().map(this::termFromExpression).toList(); - return new CoalesceAst(args); - } else if (ctx.subStringExpression() != null) { - List args = ctx.subStringExpression().expression().stream().map(this::termFromExpression).toList(); - return this.createConstraint(ASTConstants.FUNCTION_CALL.SUBSTR, args); - } else if (ctx.NOW() != null) { - return new NowAst(); - } else if (ctx.expression() != null) { - List args = ctx.expression().stream().map(this::termFromExpression).toList(); - if (ctx.STR() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.STR, args); - } else if (ctx.UCASE() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.UCASE, args); - } else if (ctx.LCASE() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.LCASE, args); - } else if (ctx.ENCODE_FOR_URI() != null) { - return new EncodeForUriAst(args); - } else if (ctx.LANG() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.LANG, args); - } else if (ctx.LANGMATCHES() != null) { - return new LangMatchesAst(args); - } else if (ctx.CONTAINS() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.CONTAINS, args); - } else if (ctx.STRSTARTS() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.STRSTARTS, args); - } else if (ctx.STRENDS() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.STRENDS, args); - } else if (ctx.STRDT() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.STRDT, args); - } else if (ctx.STRLANG() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.STRLANG, args); - } else if (ctx.STRBEFORE() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.STRBEFORE, args); - } else if (ctx.STRAFTER() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.STRAFTER, args); - } else if (ctx.YEAR() != null) { - return new YearAst(args); - } else if (ctx.MONTH() != null) { - return new MonthAst(args); - } else if (ctx.DAY() != null) { - return new DayAst(args); - } else if (ctx.HOURS() != null) { - return new HoursAst(args); - } else if (ctx.MINUTES() != null) { - return new MinutesAst(args); - } else if (ctx.SECONDS() != null) { - return new SecondsAst(args); - } else if (ctx.TIMEZONE() != null) { - return new TimezoneAst(args); - } else if (ctx.TZ() != null) { - return new TzAst(args); - } else if (ctx.DATATYPE() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.DATATYPE, args); - } else if (ctx.IRI() != null || ctx.URI() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.IRI, args); - } else if (ctx.SAME_TERM() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.SAMETERM, args); - } else if (ctx.IS_URI() != null || ctx.IS_IRI() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.IS_IRI, args); - } else if (ctx.IS_BLANK() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.IS_BLANK, args); - } else if (ctx.IS_LITERAL() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.IS_LITERAL, args); - } else if (ctx.MD5() != null) { - return new Md5Ast(args); - } else if (ctx.SHA1() != null) { - return new Sha1Ast(args); - } else if (ctx.SHA256() != null) { - return new Sha256Ast(args); - } else if (ctx.SHA384() != null) { - return new Sha384Ast(args); - } else if (ctx.SHA512() != null) { - return new Sha512Ast(args); - } else if (ctx.ABS() != null) { - return new AbsAst(args); - } else if (ctx.CEIL() != null) { - return new CeilAst(args); - } else if (ctx.FLOOR() != null) { - return new FloorAst(args); - } else if (ctx.ROUND() != null) { - return new RoundAst(args); - } else if (ctx.BOUND() != null) { - return this.createConstraint(ASTConstants.FUNCTION_CALL.BOUND, List.of(this.var(ctx.var_().getText()))); - } else if (ctx.regexExpression() != null) { - return termFromRegex(ctx.regexExpression()); - } else if (ctx.STRLEN() != null) { - return new StrLenAst(args.getFirst()); - } else { - throw new QueryEvaluationException("Unexpected function for a BuiltInCall for token " + ctx.getText()); - } - } else { - throw new QueryEvaluationException("Unable to resolve BuiltInCall for token " + ctx.getText()); - } + return expressions.termFromConstraint(ctx); } public TermAst termFromExpression(SparqlParser.ExpressionContext ctx) { - if (ctx.conditionalOrExpression() != null) { - return this.termFromConditionalOr(ctx.conditionalOrExpression()); - } else { - throw new QueryEvaluationException("No conditional OR found"); - } - } - - public TermAst termFromConditionalOr(SparqlParser.ConditionalOrExpressionContext ctx) { - if (ctx.conditionalAndExpression() != null && !ctx.conditionalAndExpression().isEmpty()) { - if (ctx.conditionalAndExpression().size() > 1) { - List args = ctx.conditionalAndExpression().stream().map(this::termFromConditionalAnd).toList(); - return createConstraint(ASTConstants.OPERATOR.OR, args); - } else { - return termFromConditionalAnd(ctx.conditionalAndExpression().getFirst()); - } - } else { - throw new QueryEvaluationException("No conditional AND found"); - } - } - - public TermAst termFromConditionalAnd(SparqlParser.ConditionalAndExpressionContext ctx) { - if (ctx.valueLogical() != null && !ctx.valueLogical().isEmpty()) { - if (ctx.valueLogical().size() > 1) { - List args = ctx.valueLogical().stream().map(this::termFromValueLogical).toList(); - return createConstraint(ASTConstants.OPERATOR.AND, args); - } else { - return termFromValueLogical(ctx.valueLogical().getFirst()); - } - } else { - throw new QueryEvaluationException("No logical value found"); - } - } - - public TermAst termFromValueLogical(SparqlParser.ValueLogicalContext ctx) { - if (ctx.relationalExpression() != null) { - return this.termFromRelational(ctx.relationalExpression()); - } else { - throw new QueryEvaluationException("No relational termFromExpression found"); - } - } - - public TermAst termFromRelational(SparqlParser.RelationalExpressionContext ctx) { - if (ctx.numericExpression() == null || ctx.numericExpression().isEmpty()) { - throw new QueryEvaluationException("No numeric termFromExpression found"); - } - TermAst lhs = termFromNumeric(ctx.numericExpression().getFirst()); - - if (ctx.IN() != null) { - List candidates = termFromExpressionList(ctx.expressionList()); - if (ctx.NOT() != null) { - return new NotInAst(lhs, candidates); - } - return new InAst(lhs, candidates); - } - - if (ctx.numericExpression().size() > 1) { - ASTConstants.OPERATOR op; - if (ctx.EQUAL() != null) { - op = ASTConstants.OPERATOR.EQ; - } else if (ctx.NOT_EQUAL() != null) { - op = ASTConstants.OPERATOR.NE; - } else if (ctx.LESS() != null) { - op = ASTConstants.OPERATOR.LT; - } else if (ctx.LESS_OR_EQUAL() != null) { - op = ASTConstants.OPERATOR.LE; - } else if (ctx.GREATER() != null) { - op = ASTConstants.OPERATOR.GT; - } else if (ctx.GREATER_OR_EQUAL() != null) { - op = ASTConstants.OPERATOR.GE; - } else { - throw new QueryEvaluationException("Unexpected operator in relational termFromExpression"); - } - List args = ctx.numericExpression().stream().map(this::termFromNumeric).toList(); - return createConstraint(op, args); - } - - return lhs; - } - - /** - * Right-hand side of {@code IN} / {@code NOT IN}: {@code NIL} ({@code ()}) or parenthesized expression list. - */ - public List termFromExpressionList(SparqlParser.ExpressionListContext ctx) { - if (ctx == null) { - throw new QueryEvaluationException("expressionList missing for IN / NOT IN"); - } - if (ctx.NIL() != null) { - return List.of(); - } - if (ctx.expression() != null && !ctx.expression().isEmpty()) { - return ctx.expression().stream().map(this::termFromExpression).toList(); - } - return List.of(); - } - - public TermAst termFromNumeric(SparqlParser.NumericExpressionContext ctx) { - if (ctx.additiveExpression() != null) { - return this.termFromAdditive(ctx.additiveExpression()); - } else { - throw new QueryEvaluationException("No additive termFromExpression found"); - } - } - - public TermAst termFromAdditive(SparqlParser.AdditiveExpressionContext ctx) { - if (ctx.multiplicativeExpression() != null && !ctx.multiplicativeExpression().isEmpty()) { - if (ctx.multiplicativeExpression().size() > 1 - || !ctx.numericLiteralNegative().isEmpty() - || !ctx.numericLiteralPositive().isEmpty()) { - TermAst leftHand = termFromMultiplicative(ctx.multiplicativeExpression().getFirst()); - ASTConstants.OPERATOR op = ASTConstants.OPERATOR.ADD; - for (int i = 1; i < ctx.getChildCount(); i++) { - ParseTree child = ctx.getChild(i); - if (child instanceof TerminalNode) { - if (Objects.equals(child.getText(), "+")) { - op = ASTConstants.OPERATOR.ADD; - } else if (Objects.equals(child.getText(), "-")) { - op = ASTConstants.OPERATOR.SUB; - } else { - throw new QueryEvaluationException("Unexpected operator in additive termFromExpression " + child.getText()); - } - } else { - TermAst rightHand = switch (child) { - case SparqlParser.MultiplicativeExpressionContext multiplicativeExpressionContext -> - termFromMultiplicative(multiplicativeExpressionContext); - case SparqlParser.NumericLiteralPositiveContext numericLiteralPositiveContext -> - (ExprAst) termFromNumericLiteralPositive(numericLiteralPositiveContext); - case SparqlParser.NumericLiteralNegativeContext numericLiteralNegativeContext -> - (ExprAst) termFromNumericLiteralNegative(numericLiteralNegativeContext); - case null, default -> - throw new QueryEvaluationException( - "Unexpected left hand termFromExpression in additive termFromExpression " - + ctx.getText() - + " " - + (child != null ? child.getText() : "null") - ); - }; - leftHand = createConstraint(op, List.of(leftHand, rightHand)); - } - } - return leftHand; - } else { - return termFromMultiplicative(ctx.multiplicativeExpression().getFirst()); - } - } else { - throw new QueryEvaluationException("No multiplicative termFromExpression found"); - } - } - - public TermAst termFromMultiplicative(SparqlParser.MultiplicativeExpressionContext ctx) { - if (ctx.unaryExpression() != null && !ctx.unaryExpression().isEmpty()) { - if (ctx.unaryExpression().size() > 1) { - TermAst head = termFromUnary(ctx.unaryExpression().getFirst()); - TermAst rightHand = null; - ASTConstants.OPERATOR op = null; - for (int i = 1; i < ctx.getChildCount(); i+=2) { - ParseTree operatorContext = ctx.getChild(i); - ParseTree rightHandContext = ctx.getChild(i+1); - - if(rightHandContext instanceof SparqlParser.UnaryExpressionContext unaryExpressionContext) { - rightHand = termFromUnary( unaryExpressionContext); - } - - if(operatorContext instanceof TerminalNode terminalNode) { - if(Objects.equals(terminalNode.getText(), "*")) { - op = ASTConstants.OPERATOR.MUL; - } else if(Objects.equals(terminalNode.getText(), "/")) { - op = ASTConstants.OPERATOR.DIV; - } - } - if(op != null && rightHand != null) { - head = createConstraint(op, List.of(head, rightHand)); - } else { - throw new QuerySyntaxException("Unexpected operator or right hand content in " + ctx.getText()); - } - } - return head; - } else { - return termFromUnary(ctx.unaryExpression().getFirst()); - } - } else { - throw new QueryEvaluationException("No unary termFromExpression found"); - } - } - - public TermAst termFromUnary(SparqlParser.UnaryExpressionContext ctx) { - ASTConstants.Constraint op = null; - if (ctx.PLUS() != null) { - op = ASTConstants.OPERATOR.PLUS; - } else if (ctx.MINUS_SIGN() != null) { - op = ASTConstants.OPERATOR.MINUS; - } else if (ctx.EXCLAMATION() != null) { - op = ASTConstants.OPERATOR.NOT; - } - if (op != null) { - return createConstraint(op, List.of(termFromPrimary(ctx.primaryExpression()))); - } else { - return termFromPrimary(ctx.primaryExpression()); - } + return expressions.termFromExpression(ctx); } public TermAst termFromBrackettedExpression(SparqlParser.BrackettedExpressionContext ctx) { - return termFromExpression(ctx.expression()); - } - - public TermAst termFromRegex(SparqlParser.RegexExpressionContext ctx) { - if (ctx.expression() != null) { - List args = ctx.expression().stream().map(this::termFromExpression).toList(); - return this.createConstraint(ASTConstants.FUNCTION_CALL.REGEX, args); - } else { - throw new QueryEvaluationException("Unexpected arguments for REGEX call"); - } - } - - public TermAst termFromReplace(SparqlParser.StrReplaceExpressionContext ctx) { - if (ctx.expression() != null) { - List args = ctx.expression().stream().map(this::termFromExpression).toList(); - return this.createConstraint(ASTConstants.FUNCTION_CALL.REPLACE, args); - } else { - throw new QueryEvaluationException("Unexpected arguments for REPLACE call"); - } + return expressions.termFromBrackettedExpression(ctx); } /** * Predicate as a SPARQL 1.1 property path. */ public PathAst pathFromVerbPath(SparqlParser.VerbPathContext ctx) { - return pathFromPath(ctx.path()); - } - - public PathAst pathFromPath(SparqlParser.PathContext ctx) { - return pathFromPathAlternative(ctx.pathAlternative()); - } - - private PathAst pathFromPathAlternative(SparqlParser.PathAlternativeContext ctx) { - return foldAlternatives(ctx.pathSequence().stream().map(this::pathFromPathSequence).toList()); - } - - private PathAst pathFromPathSequence(SparqlParser.PathSequenceContext ctx) { - return foldSequences(ctx.pathEltOrInverse().stream().map(this::pathFromPathEltOrInverse).toList()); - } - - private PathAst pathFromPathEltOrInverse(SparqlParser.PathEltOrInverseContext ctx) { - if (ctx.CARET() != null) { - return new InversePathAst(pathFromPathElt(ctx.pathElt())); - } - return pathFromPathElt(ctx.pathElt()); - } - - private PathAst pathFromPathElt(SparqlParser.PathEltContext ctx) { - PathAst primary = pathFromPathPrimary(ctx.pathPrimary()); - SparqlParser.PathModContext mod = ctx.pathMod(); - if (mod == null) { - return primary; - } - if (mod.QUESTION() != null) { - return new OptionalPathAst(primary); - } - if (mod.STAR() != null) { - return new ZeroOrMorePathAst(primary); - } - if (mod.PLUS() != null) { - return new OneOrMorePathAst(primary); - } - throw new QueryEvaluationException("Unexpected path modifier in " + ctx.getText()); - } - - private PathAst pathFromPathPrimary(SparqlParser.PathPrimaryContext ctx) { - if (ctx.iriRef() != null) { - return new PredicatePathAst(termFromIriRef(ctx.iriRef())); - } - if (ctx.A() != null) { - return new PredicatePathAst(iri("a")); - } - if (ctx.EXCLAMATION() != null) { - return pathFromPathNegatedPropertySet(ctx.pathNegatedPropertySet()); - } - if (ctx.path() != null) { - return pathFromPath(ctx.path()); - } - throw new QueryEvaluationException("Unexpected path primary in " + ctx.getText()); - } - - private PathAst pathFromPathNegatedPropertySet(SparqlParser.PathNegatedPropertySetContext ctx) { - List excluded; - if (ctx.L_PAREN() != null) { - excluded = ctx.pathOneInPropertySet().stream().map(this::pathFromPathOneInPropertySet).toList(); - } else { - excluded = List.of(pathFromPathOneInPropertySet(ctx.pathOneInPropertySet().getFirst())); - } - return new NegatedPropertySetPathAst(excluded); - } - - private PathAst pathFromPathOneInPropertySet(SparqlParser.PathOneInPropertySetContext ctx) { - if (ctx.CARET() != null) { - if (ctx.iriRef() != null) { - return new InversePathAst(new PredicatePathAst(termFromIriRef(ctx.iriRef()))); - } - return new InversePathAst(new PredicatePathAst(iri("a"))); - } - if (ctx.iriRef() != null) { - return new PredicatePathAst(termFromIriRef(ctx.iriRef())); - } - return new PredicatePathAst(iri("a")); - } - - private PathAst foldAlternatives(List parts) { - if (parts.isEmpty()) { - throw new QueryEvaluationException("Empty property path alternative"); - } - PathAst result = parts.getFirst(); - for (int i = 1; i < parts.size(); i++) { - result = new AlternativePathAst(result, parts.get(i)); - } - return result; - } - - private PathAst foldSequences(List parts) { - if (parts.isEmpty()) { - throw new QueryEvaluationException("Empty property path sequence"); - } - PathAst result = parts.getFirst(); - for (int i = 1; i < parts.size(); i++) { - result = new SequencePathAst(result, parts.get(i)); - } - return result; + return paths.pathFromVerbPath(ctx); } /** @@ -1445,7 +695,7 @@ private PathAst predicateFromPropertyListPath( if (useVerbPath) { return pathFromVerbPath(verbPaths.get(verbPathIdx)); } - return new PredicatePathAst(termFromVerbSimple(verbSimples.get(verbSimpleIdx))); + return new PredicatePathAst(terms.termFromVerbSimple(verbSimples.get(verbSimpleIdx))); } /** @@ -1508,13 +758,6 @@ private TermAst rdfTerm(RDF term) { return new IriAst("<" + term.getIRI().stringValue() + ">"); } - /** - * Predicate as a simple variable (e.g. ?p used as a predicate). - */ - public TermAst termFromVerbSimple(SparqlParser.VerbSimpleContext ctx) { - return termFromVar(ctx.var_()); - } - /** * List of objects inside a property path triple. */ @@ -1526,45 +769,15 @@ public List termListFromObjectListPath(SparqlParser.ObjectListPathConte return out; } - public TermAst termFromAggregate(SparqlParser.AggregateContext ctx) { - boolean distinct = ctx.DISTINCT() != null; - - if (ctx.COUNT() != null) { - TermAst expression = null; - - if (ctx.STAR() == null && ctx.expression() != null && !ctx.expression().isEmpty()) { - expression = termFromExpression(ctx.expression()); - } - return createAggregate(AggregateFunction.COUNT, distinct, expression, null); - } - - if (ctx.SUM() != null) { - return createAggregate(AggregateFunction.SUM, distinct, termFromExpression(ctx.expression()), null); - } - - if (ctx.AVG() != null) { - return createAggregate(AggregateFunction.AVG, distinct, termFromExpression(ctx.expression()), null); - } - - if (ctx.MIN() != null) { - return createAggregate(AggregateFunction.MIN, distinct, termFromExpression(ctx.expression()), null); - } - - if (ctx.MAX() != null) { - return createAggregate(AggregateFunction.MAX, distinct, termFromExpression(ctx.expression()), null); - } - - if (ctx.SAMPLE() != null) { - return createAggregate(AggregateFunction.SAMPLE, distinct, termFromExpression(ctx.expression()), null); + public GraphRefAst graphRefFromGraphOrDefault(SparqlParser.GraphOrDefaultContext ctx) { + if (ctx.DEFAULT() != null) { + return GraphRefAsts.defaultGraph(); } - - if (ctx.GROUP_CONCAT() != null) { - String sep = ctx.string_() != null ? ctx.string_().getText() : null; - return createAggregate( - AggregateFunction.GROUP_CONCAT, distinct, termFromExpression(ctx.expression()), sep); + if(ctx.iriRef() != null) { + IriAst graphIri = (IriAst) termFromIriRef(ctx.iriRef()); + return GraphRefAsts.graph(graphIri); } - - throw new QueryEvaluationException("Unsupported aggregate: " + ctx.getText()); + throw new QueryEvaluationException("Unexpected value for Graph reference or default " + ctx.getText()); } public GraphRefAst graphRefFromGraphRef(SparqlParser.GraphRefContext ctx) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlExpressionBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlExpressionBuilder.java new file mode 100644 index 000000000..23ff6ed64 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlExpressionBuilder.java @@ -0,0 +1,635 @@ +package fr.inria.corese.core.next.query.impl.parser; + +import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; +import fr.inria.corese.core.next.query.impl.sparql.ast.*; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; + +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Expression and constraint building helpers extracted from {@link SparqlAstBuilder}. + *

+ * The only runtime dependency on {@link SparqlAstBuilder} state is the captured + * EXISTS/NOT EXISTS group-graph-pattern, accessed through the {@code existsPatternSupplier} + * callback injected at construction time. + */ +public final class SparqlExpressionBuilder { + + private final SparqlTermBuilder terms; + private final Supplier existsPatternSupplier; + + public SparqlExpressionBuilder(SparqlTermBuilder terms, + Supplier existsPatternSupplier) { + this.terms = terms; + this.existsPatternSupplier = existsPatternSupplier; + } + + // --- Constraint / function-call factories --- + + /** + * Creates the right AST node for a built-in SPARQL operator or function. + */ + public ConstraintAst createConstraint(ASTConstants.Constraint constraint, List args) { + switch (constraint) { + case ASTConstants.OPERATOR.NOT -> { return new BooleanNotAst(args); } + case ASTConstants.OPERATOR.PLUS -> { return new UnaryPlusAst(args); } + case ASTConstants.OPERATOR.MINUS -> { return new UnaryMinusAst(args); } + case ASTConstants.FUNCTION_CALL.BOUND -> { return new BoundAst(args); } + case ASTConstants.FUNCTION_CALL.IS_IRI -> { return new IsIriAst(args); } + case ASTConstants.FUNCTION_CALL.IS_BLANK -> { return new IsBlankAst(args); } + case ASTConstants.FUNCTION_CALL.IS_LITERAL -> { return new IsLiteralAst(args); } + case ASTConstants.FUNCTION_CALL.STR -> { return new StrAst(args); } + case ASTConstants.FUNCTION_CALL.UCASE -> { return new UcaseAst(args); } + case ASTConstants.FUNCTION_CALL.LCASE -> { return new LcaseAst(args); } + case ASTConstants.FUNCTION_CALL.LANG -> { return new LangAst(args); } + case ASTConstants.FUNCTION_CALL.STRDT -> { return new StrDtAst(args); } + case ASTConstants.FUNCTION_CALL.STRLANG -> { return new StrLangAst(args); } + case ASTConstants.FUNCTION_CALL.DATATYPE -> { return new DatatypeAst(args); } + case ASTConstants.FUNCTION_CALL.IRI -> { return new IriFunctionAst(args); } + case ASTConstants.FUNCTION_CALL.BNODE -> { return new BnodeAst(args); } + case ASTConstants.OPERATOR.OR -> { return new OrAst(args); } + case ASTConstants.OPERATOR.AND -> { return new AndAst(args); } + case ASTConstants.OPERATOR.EQ -> { return new EqualsAst(args); } + case ASTConstants.OPERATOR.NE -> { return new DifferentAst(args); } + case ASTConstants.OPERATOR.LT -> { return new LowerThanAst(args); } + case ASTConstants.OPERATOR.LE -> { return new LowerOrEqualThanAst(args); } + case ASTConstants.OPERATOR.GT -> { return new GreaterThanAst(args); } + case ASTConstants.OPERATOR.GE -> { return new GreaterOrEqualThanAst(args); } + case ASTConstants.OPERATOR.MUL -> { return new MultiplyAst(args); } + case ASTConstants.OPERATOR.DIV -> { return new DivideAst(args); } + case ASTConstants.OPERATOR.ADD -> { return new AddAst(args); } + case ASTConstants.OPERATOR.SUB -> { return new SubtractAst(args); } + case ASTConstants.FUNCTION_CALL.SAMETERM -> { return new SameTermAst(args); } + case ASTConstants.FUNCTION_CALL.LANGMATCHES -> { return new LangMatchesAst(args); } + case ASTConstants.FUNCTION_CALL.CONTAINS -> { return new ContainsAst(args); } + case ASTConstants.FUNCTION_CALL.STRSTARTS -> { return new StrStartsAst(args); } + case ASTConstants.FUNCTION_CALL.STRENDS -> { return new StrEndsAst(args); } + case ASTConstants.FUNCTION_CALL.SUBSTR -> { return new SubstrAst(args); } + case ASTConstants.FUNCTION_CALL.CONCAT -> { return new ConcatAst(args); } + case ASTConstants.FUNCTION_CALL.STRBEFORE -> { return new StrBeforeAst(args); } + case ASTConstants.FUNCTION_CALL.STRAFTER -> { return new StrAfterAst(args); } + case ASTConstants.FUNCTION_CALL.REPLACE -> { return new ReplaceAst(args); } + case ASTConstants.FUNCTION_CALL.REGEX -> { + if (args.size() == 2) { + return new BinaryRegexAst(args); + } else if (args.size() == 3) { + return new TrinaryRegexAst(args); + } else { + throw new QueryEvaluationException("Unexpected number of arguments (3) for REGEX keyword"); + } + } + default -> + throw new QueryEvaluationException( + "Unexpected number of arguments (" + args.size() + ") for " + constraint); + } + } + + public ConstraintAst createFunCall(IriAst functionName, List args) { + return new FunctionCallAst(functionName, args); + } + + public AggregateAst createAggregate( + AggregateFunction function, boolean distinct, TermAst expression, String groupConcatSeparator) { + return new AggregateAst(function, distinct, expression, groupConcatSeparator); + } + + // --- ANTLR expression converters --- + + public TermAst termFromExpression(SparqlParser.ExpressionContext ctx) { + if (ctx.conditionalOrExpression() != null) { + return termFromConditionalOr(ctx.conditionalOrExpression()); + } else { + throw new QueryEvaluationException("No conditional OR found"); + } + } + + public TermAst termFromConditionalOr(SparqlParser.ConditionalOrExpressionContext ctx) { + if (ctx.conditionalAndExpression() != null && !ctx.conditionalAndExpression().isEmpty()) { + if (ctx.conditionalAndExpression().size() > 1) { + List args = ctx.conditionalAndExpression().stream() + .map(this::termFromConditionalAnd).toList(); + return createConstraint(ASTConstants.OPERATOR.OR, args); + } else { + return termFromConditionalAnd(ctx.conditionalAndExpression().getFirst()); + } + } else { + throw new QueryEvaluationException("No conditional AND found"); + } + } + + public TermAst termFromConditionalAnd(SparqlParser.ConditionalAndExpressionContext ctx) { + if (ctx.valueLogical() != null && !ctx.valueLogical().isEmpty()) { + if (ctx.valueLogical().size() > 1) { + List args = ctx.valueLogical().stream().map(this::termFromValueLogical).toList(); + return createConstraint(ASTConstants.OPERATOR.AND, args); + } else { + return termFromValueLogical(ctx.valueLogical().getFirst()); + } + } else { + throw new QueryEvaluationException("No logical value found"); + } + } + + public TermAst termFromValueLogical(SparqlParser.ValueLogicalContext ctx) { + if (ctx.relationalExpression() != null) { + return termFromRelational(ctx.relationalExpression()); + } else { + throw new QueryEvaluationException("No relational termFromExpression found"); + } + } + + public TermAst termFromRelational(SparqlParser.RelationalExpressionContext ctx) { + if (ctx.numericExpression() == null || ctx.numericExpression().isEmpty()) { + throw new QueryEvaluationException("No numeric termFromExpression found"); + } + TermAst lhs = termFromNumeric(ctx.numericExpression().getFirst()); + + if (ctx.IN() != null) { + List candidates = termFromExpressionList(ctx.expressionList()); + if (ctx.NOT() != null) { + return new NotInAst(lhs, candidates); + } + return new InAst(lhs, candidates); + } + + if (ctx.numericExpression().size() > 1) { + ASTConstants.OPERATOR op; + if (ctx.EQUAL() != null) { + op = ASTConstants.OPERATOR.EQ; + } else if (ctx.NOT_EQUAL() != null) { + op = ASTConstants.OPERATOR.NE; + } else if (ctx.LESS() != null) { + op = ASTConstants.OPERATOR.LT; + } else if (ctx.LESS_OR_EQUAL() != null) { + op = ASTConstants.OPERATOR.LE; + } else if (ctx.GREATER() != null) { + op = ASTConstants.OPERATOR.GT; + } else if (ctx.GREATER_OR_EQUAL() != null) { + op = ASTConstants.OPERATOR.GE; + } else { + throw new QueryEvaluationException("Unexpected operator in relational termFromExpression"); + } + List args = ctx.numericExpression().stream().map(this::termFromNumeric).toList(); + return createConstraint(op, args); + } + + return lhs; + } + + /** + * Right-hand side of {@code IN} / {@code NOT IN}: {@code NIL} ({@code ()}) or parenthesized expression list. + */ + public List termFromExpressionList(SparqlParser.ExpressionListContext ctx) { + if (ctx == null) { + throw new QueryEvaluationException("expressionList missing for IN / NOT IN"); + } + if (ctx.NIL() != null) { + return List.of(); + } + if (ctx.expression() != null && !ctx.expression().isEmpty()) { + return ctx.expression().stream().map(this::termFromExpression).toList(); + } + return List.of(); + } + + public TermAst termFromNumeric(SparqlParser.NumericExpressionContext ctx) { + if (ctx.additiveExpression() != null) { + return termFromAdditive(ctx.additiveExpression()); + } else { + throw new QueryEvaluationException("No additive termFromExpression found"); + } + } + + public TermAst termFromAdditive(SparqlParser.AdditiveExpressionContext ctx) { + if (ctx.multiplicativeExpression() != null && !ctx.multiplicativeExpression().isEmpty()) { + if (ctx.multiplicativeExpression().size() > 1 + || !ctx.numericLiteralNegative().isEmpty() + || !ctx.numericLiteralPositive().isEmpty()) { + TermAst leftHand = termFromMultiplicative(ctx.multiplicativeExpression().getFirst()); + ASTConstants.OPERATOR op = ASTConstants.OPERATOR.ADD; + for (int i = 1; i < ctx.getChildCount(); i++) { + ParseTree child = ctx.getChild(i); + if (child instanceof TerminalNode) { + if (Objects.equals(child.getText(), "+")) { + op = ASTConstants.OPERATOR.ADD; + } else if (Objects.equals(child.getText(), "-")) { + op = ASTConstants.OPERATOR.SUB; + } else { + throw new QueryEvaluationException( + "Unexpected operator in additive termFromExpression " + child.getText()); + } + } else { + TermAst rightHand = switch (child) { + case SparqlParser.MultiplicativeExpressionContext c -> + termFromMultiplicative(c); + case SparqlParser.NumericLiteralPositiveContext c -> + (ExprAst) terms.termFromNumericLiteralPositive(c); + case SparqlParser.NumericLiteralNegativeContext c -> + (ExprAst) terms.termFromNumericLiteralNegative(c); + case null, default -> + throw new QueryEvaluationException( + "Unexpected left hand termFromExpression in additive termFromExpression " + + ctx.getText() + + " " + + (child != null ? child.getText() : "null") + ); + }; + leftHand = createConstraint(op, List.of(leftHand, rightHand)); + } + } + return leftHand; + } else { + return termFromMultiplicative(ctx.multiplicativeExpression().getFirst()); + } + } else { + throw new QueryEvaluationException("No multiplicative termFromExpression found"); + } + } + + public TermAst termFromMultiplicative(SparqlParser.MultiplicativeExpressionContext ctx) { + if (ctx.unaryExpression() != null && !ctx.unaryExpression().isEmpty()) { + if (ctx.unaryExpression().size() > 1) { + TermAst head = termFromUnary(ctx.unaryExpression().getFirst()); + TermAst rightHand = null; + ASTConstants.OPERATOR op = null; + for (int i = 1; i < ctx.getChildCount(); i += 2) { + ParseTree operatorContext = ctx.getChild(i); + ParseTree rightHandContext = ctx.getChild(i + 1); + + if (rightHandContext instanceof SparqlParser.UnaryExpressionContext uec) { + rightHand = termFromUnary(uec); + } + + if (operatorContext instanceof TerminalNode terminalNode) { + if (Objects.equals(terminalNode.getText(), "*")) { + op = ASTConstants.OPERATOR.MUL; + } else if (Objects.equals(terminalNode.getText(), "/")) { + op = ASTConstants.OPERATOR.DIV; + } + } + if (op != null && rightHand != null) { + head = createConstraint(op, List.of(head, rightHand)); + } else { + throw new QuerySyntaxException( + "Unexpected operator or right hand content in " + ctx.getText()); + } + } + return head; + } else { + return termFromUnary(ctx.unaryExpression().getFirst()); + } + } else { + throw new QueryEvaluationException("No unary termFromExpression found"); + } + } + + public TermAst termFromUnary(SparqlParser.UnaryExpressionContext ctx) { + ASTConstants.Constraint op = null; + if (ctx.PLUS() != null) { + op = ASTConstants.OPERATOR.PLUS; + } else if (ctx.MINUS_SIGN() != null) { + op = ASTConstants.OPERATOR.MINUS; + } else if (ctx.EXCLAMATION() != null) { + op = ASTConstants.OPERATOR.NOT; + } + if (op != null) { + return createConstraint(op, List.of(termFromPrimary(ctx.primaryExpression()))); + } else { + return termFromPrimary(ctx.primaryExpression()); + } + } + + public TermAst termFromBrackettedExpression(SparqlParser.BrackettedExpressionContext ctx) { + return termFromExpression(ctx.expression()); + } + + public TermAst termFromRegex(SparqlParser.RegexExpressionContext ctx) { + if (ctx.expression() != null) { + List args = ctx.expression().stream().map(this::termFromExpression).toList(); + return createConstraint(ASTConstants.FUNCTION_CALL.REGEX, args); + } else { + throw new QueryEvaluationException("Unexpected arguments for REGEX call"); + } + } + + public TermAst termFromReplace(SparqlParser.StrReplaceExpressionContext ctx) { + if (ctx.expression() != null) { + List args = ctx.expression().stream().map(this::termFromExpression).toList(); + return createConstraint(ASTConstants.FUNCTION_CALL.REPLACE, args); + } else { + throw new QueryEvaluationException("Unexpected arguments for REPLACE call"); + } + } + + /** + * Converts a {@code builtInCall} parse-tree node to the corresponding + * {@link TermAst} / {@link ConstraintAst}. + * + *

Dispatch order

+ *
    + *
  1. Aggregates ({@code COUNT}, {@code SUM}, {@code AVG}, + * {@code MIN}, {@code MAX}, {@code SAMPLE}, {@code GROUP_CONCAT}) — + * handled first via {@link #termFromAggregate}.
  2. + *
  3. EXISTS / NOT EXISTS — these two forms must be checked + * immediately after aggregates because the inner graph-pattern has already + * been captured into {@code capturedExistsStack} by the time this method is + * called (the ANTLR listener exits the nested group before exiting + * the built-in call). Consuming the captured pattern via + * {@code existsPatternSupplier.get()} here ensures correct ordering; + * delaying the check could cause the pattern to be consumed by the wrong + * call site.
  4. + *
  5. REGEX / REPLACE — delegated to their own parse-context + * helpers {@link #termFromRegex} and {@link #termFromReplace}.
  6. + *
  7. Zero-arg functions — {@code BOUND}, {@code BNODE} (0 or 1 + * arg), {@code IF}, {@code RAND}, {@code UUID}, {@code STRUUID}, + * {@code CONCAT}, {@code COALESCE}, {@code SUBSTR} (via + * {@code subStringExpression}), {@code NOW}.
  8. + *
  9. String functions — {@code STR}, {@code UCASE}, + * {@code LCASE}, {@code ENCODE_FOR_URI}, {@code LANG}, + * {@code LANGMATCHES}, {@code CONTAINS}, {@code STRSTARTS}, + * {@code STRENDS}, {@code STRDT}, {@code STRLANG}, {@code STRBEFORE}, + * {@code STRAFTER}, {@code STRLEN}.
  10. + *
  11. Date/time functions — {@code YEAR}, {@code MONTH}, + * {@code DAY}, {@code HOURS}, {@code MINUTES}, {@code SECONDS}, + * {@code TIMEZONE}, {@code TZ}.
  12. + *
  13. Type accessors — {@code DATATYPE}, {@code IRI} / + * {@code URI}.
  14. + *
  15. Type tests — {@code sameTerm}, {@code isIRI} / + * {@code isURI}, {@code isBlank}, {@code isLiteral}.
  16. + *
  17. Hash functions — {@code MD5}, {@code SHA1}, + * {@code SHA256}, {@code SHA384}, {@code SHA512}.
  18. + *
  19. Math functions — {@code ABS}, {@code CEIL}, + * {@code FLOOR}, {@code ROUND}.
  20. + *
+ * + * @param ctx the ANTLR parse-tree node for a {@code builtInCall} rule + * @return the AST node representing the built-in call + * @throws QueryEvaluationException if the function keyword is unknown or the + * EXISTS/NOT EXISTS pattern was not captured + */ + public TermAst termFromBuiltInCall(SparqlParser.BuiltInCallContext ctx) { + if (ctx.aggregate() != null) { + return termFromAggregate(ctx.aggregate()); + } + if (ctx.existsFunc() != null) { + GroupGraphPatternAst existsPattern = existsPatternSupplier.get(); + if (existsPattern == null) { + throw new QueryEvaluationException( + "EXISTS { ... } inner pattern was not captured; check listener order"); + } + return new ExistsAst(existsPattern); + } + if (ctx.notExistsFunc() != null) { + GroupGraphPatternAst notExistsPattern = existsPatternSupplier.get(); + if (notExistsPattern == null) { + throw new QueryEvaluationException( + "NOT EXISTS { ... } inner pattern was not captured; check listener order"); + } + return new NotExistsAst(notExistsPattern); + } + if (ctx.regexExpression() != null) { + return termFromRegex(ctx.regexExpression()); + } else if (ctx.strReplaceExpression() != null) { + return termFromReplace(ctx.strReplaceExpression()); + } else if (ctx.BOUND() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.BOUND, + List.of(terms.var(ctx.var_().getText()))); + } else if (ctx.BNODE() != null) { + List args = ctx.expression() == null + ? List.of() + : ctx.expression().stream().map(this::termFromExpression).toList(); + return createConstraint(ASTConstants.FUNCTION_CALL.BNODE, args); + } else if (ctx.IF() != null) { + List args = ctx.expression().stream().map(this::termFromExpression).toList(); + return new IfAst(args.get(0), args.get(1), args.get(2)); + } else if (ctx.RAND() != null) { + return new RandAst(); + } else if (ctx.UUID() != null) { + return new UuidAst(); + } else if (ctx.STRUUID() != null) { + return new StrUuidAst(); + } else if (ctx.CONCAT() != null) { + List args = ctx.expression().stream().map(this::termFromExpression).toList(); + return createConstraint(ASTConstants.FUNCTION_CALL.CONCAT, args); + } else if (ctx.COALESCE() != null) { + List args = ctx.expression().stream().map(this::termFromExpression).toList(); + return new CoalesceAst(args); + } else if (ctx.subStringExpression() != null) { + List args = ctx.subStringExpression().expression().stream() + .map(this::termFromExpression).toList(); + return createConstraint(ASTConstants.FUNCTION_CALL.SUBSTR, args); + } else if (ctx.NOW() != null) { + return new NowAst(); + } else if (ctx.expression() != null) { + List args = ctx.expression().stream().map(this::termFromExpression).toList(); + if (ctx.STR() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.STR, args); + } else if (ctx.UCASE() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.UCASE, args); + } else if (ctx.LCASE() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.LCASE, args); + } else if (ctx.ENCODE_FOR_URI() != null) { + return new EncodeForUriAst(args); + } else if (ctx.LANG() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.LANG, args); + } else if (ctx.LANGMATCHES() != null) { + return new LangMatchesAst(args); + } else if (ctx.CONTAINS() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.CONTAINS, args); + } else if (ctx.STRSTARTS() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.STRSTARTS, args); + } else if (ctx.STRENDS() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.STRENDS, args); + } else if (ctx.STRDT() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.STRDT, args); + } else if (ctx.STRLANG() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.STRLANG, args); + } else if (ctx.STRBEFORE() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.STRBEFORE, args); + } else if (ctx.STRAFTER() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.STRAFTER, args); + } else if (ctx.YEAR() != null) { + return new YearAst(args); + } else if (ctx.MONTH() != null) { + return new MonthAst(args); + } else if (ctx.DAY() != null) { + return new DayAst(args); + } else if (ctx.HOURS() != null) { + return new HoursAst(args); + } else if (ctx.MINUTES() != null) { + return new MinutesAst(args); + } else if (ctx.SECONDS() != null) { + return new SecondsAst(args); + } else if (ctx.TIMEZONE() != null) { + return new TimezoneAst(args); + } else if (ctx.TZ() != null) { + return new TzAst(args); + } else if (ctx.DATATYPE() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.DATATYPE, args); + } else if (ctx.IRI() != null || ctx.URI() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.IRI, args); + } else if (ctx.SAME_TERM() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.SAMETERM, args); + } else if (ctx.IS_URI() != null || ctx.IS_IRI() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.IS_IRI, args); + } else if (ctx.IS_BLANK() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.IS_BLANK, args); + } else if (ctx.IS_LITERAL() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.IS_LITERAL, args); + } else if (ctx.MD5() != null) { + return new Md5Ast(args); + } else if (ctx.SHA1() != null) { + return new Sha1Ast(args); + } else if (ctx.SHA256() != null) { + return new Sha256Ast(args); + } else if (ctx.SHA384() != null) { + return new Sha384Ast(args); + } else if (ctx.SHA512() != null) { + return new Sha512Ast(args); + } else if (ctx.ABS() != null) { + return new AbsAst(args); + } else if (ctx.CEIL() != null) { + return new CeilAst(args); + } else if (ctx.FLOOR() != null) { + return new FloorAst(args); + } else if (ctx.ROUND() != null) { + return new RoundAst(args); + } else if (ctx.BOUND() != null) { + return createConstraint(ASTConstants.FUNCTION_CALL.BOUND, + List.of(terms.var(ctx.var_().getText()))); + } else if (ctx.regexExpression() != null) { + return termFromRegex(ctx.regexExpression()); + } else if (ctx.STRLEN() != null) { + return new StrLenAst(args.getFirst()); + } else { + throw new QueryEvaluationException( + "Unexpected function for a BuiltInCall for token " + ctx.getText()); + } + } else { + throw new QueryEvaluationException( + "Unable to resolve BuiltInCall for token " + ctx.getText()); + } + } + + public TermAst termFromConstraint(SparqlParser.ConstraintContext ctx) { + if (ctx.builtInCall() != null) { + return termFromBuiltInCall(ctx.builtInCall()); + } else if (ctx.functionCall() != null) { + IriAst functionTermAst = new IriAst(ctx.functionCall().iriRef().getText()); + List args = ctx.functionCall().argList().expression().stream() + .map(this::termFromExpression).toList(); + return new FunctionCallAst(functionTermAst, args); + } else if (ctx.brackettedExpression() != null + && ctx.brackettedExpression().expression() != null) { + return termFromBrackettedExpression(ctx.brackettedExpression()); + } else { + throw new QueryEvaluationException("No createFunCall found in filter"); + } + } + + public TermAst termFromGroupCondition(SparqlParser.GroupConditionContext ctx) { + if (ctx.expression() != null) { + return termFromExpression(ctx.expression()); + } + if (ctx.var_() != null) { + return terms.termFromVar(ctx.var_()); + } + if (ctx.builtInCall() != null) { + return termFromBuiltInCall(ctx.builtInCall()); + } + if (ctx.functionCall() != null) { + SparqlParser.FunctionCallContext fc = ctx.functionCall(); + IriAst functionName = new IriAst(fc.iriRef().getText()); + if (fc.argList() == null) { + return createFunCall(functionName, List.of()); + } + if (fc.argList().NIL() != null) { + return createFunCall(functionName, List.of()); + } + List args = fc.argList().expression().stream() + .map(this::termFromExpression).toList(); + return createFunCall(functionName, args); + } + throw new QueryEvaluationException("Unsupported group condition: " + ctx.getText()); + } + + public TermAst termFromIriRefOrFunction(SparqlParser.IriRefOrFunctionContext ctx) { + if (ctx.iriRef() != null && ctx.argList() == null) { + return terms.termFromIriRef(ctx.iriRef()); + } else if (ctx.iriRef() != null && ctx.argList() != null) { + List args = termListFromArgList(ctx.argList()); + IriAst iriRef = (IriAst) terms.termFromIriRef(ctx.iriRef()); + return createFunCall(iriRef, args); + } else { + throw new QueryEvaluationException("Unexpected element in IRI ref or function"); + } + } + + public List termListFromArgList(SparqlParser.ArgListContext ctx) { + return ctx.expression().stream().map(this::termFromExpression).toList(); + } + + public TermAst termFromAggregate(SparqlParser.AggregateContext ctx) { + boolean distinct = ctx.DISTINCT() != null; + + if (ctx.COUNT() != null) { + TermAst expression = null; + if (ctx.STAR() == null && ctx.expression() != null && !ctx.expression().isEmpty()) { + expression = termFromExpression(ctx.expression()); + } + return createAggregate(AggregateFunction.COUNT, distinct, expression, null); + } + if (ctx.SUM() != null) { + return createAggregate(AggregateFunction.SUM, distinct, + termFromExpression(ctx.expression()), null); + } + if (ctx.AVG() != null) { + return createAggregate(AggregateFunction.AVG, distinct, + termFromExpression(ctx.expression()), null); + } + if (ctx.MIN() != null) { + return createAggregate(AggregateFunction.MIN, distinct, + termFromExpression(ctx.expression()), null); + } + if (ctx.MAX() != null) { + return createAggregate(AggregateFunction.MAX, distinct, + termFromExpression(ctx.expression()), null); + } + if (ctx.SAMPLE() != null) { + return createAggregate(AggregateFunction.SAMPLE, distinct, + termFromExpression(ctx.expression()), null); + } + if (ctx.GROUP_CONCAT() != null) { + String sep = ctx.string_() != null ? ctx.string_().getText() : null; + return createAggregate(AggregateFunction.GROUP_CONCAT, distinct, + termFromExpression(ctx.expression()), sep); + } + throw new QueryEvaluationException("Unsupported aggregate: " + ctx.getText()); + } + + public TermAst termFromPrimary(SparqlParser.PrimaryExpressionContext ctx) { + if (ctx.brackettedExpression() != null) { + return termFromBrackettedExpression(ctx.brackettedExpression()); + } else if (ctx.builtInCall() != null) { + return termFromBuiltInCall(ctx.builtInCall()); + } else if (ctx.iriRefOrFunction() != null) { + return termFromIriRefOrFunction(ctx.iriRefOrFunction()); + } else if (ctx.rdfLiteral() != null) { + return terms.termFromRdfLiteral(ctx.rdfLiteral()); + } else if (ctx.numericLiteral() != null) { + return terms.termFromNumericLiteral(ctx.numericLiteral()); + } else if (ctx.booleanLiteral() != null) { + return terms.termFromBooleanLiteral(ctx.booleanLiteral()); + } else if (ctx.var_() != null) { + return terms.termFromVar(ctx.var_()); + } else { + throw new QueryEvaluationException("Unexpected content of bracketed termFromExpression"); + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlPathBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlPathBuilder.java new file mode 100644 index 000000000..9f474a1f0 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlPathBuilder.java @@ -0,0 +1,151 @@ +package fr.inria.corese.core.next.query.impl.parser; + +import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.*; + +import java.util.List; + +/** + * Property-path building helpers extracted from {@link SparqlAstBuilder}. + *

+ * All methods are pure functions of the ANTLR parse-tree contexts; the only + * dependency on shared state is {@link SparqlTermBuilder} (needed to create + * IRI terms for path predicates). + */ +public final class SparqlPathBuilder { + + private final SparqlTermBuilder terms; + + public SparqlPathBuilder(SparqlTermBuilder terms) { + this.terms = terms; + } + + public PathAst pathFromVerbPath(SparqlParser.VerbPathContext ctx) { + return pathFromPath(ctx.path()); + } + + public PathAst pathFromPath(SparqlParser.PathContext ctx) { + return pathFromPathAlternative(ctx.pathAlternative()); + } + + PathAst pathFromPathAlternative(SparqlParser.PathAlternativeContext ctx) { + return foldAlternatives(ctx.pathSequence().stream().map(this::pathFromPathSequence).toList()); + } + + PathAst pathFromPathSequence(SparqlParser.PathSequenceContext ctx) { + return foldSequences(ctx.pathEltOrInverse().stream().map(this::pathFromPathEltOrInverse).toList()); + } + + PathAst pathFromPathEltOrInverse(SparqlParser.PathEltOrInverseContext ctx) { + if (ctx.CARET() != null) { + return new InversePathAst(pathFromPathElt(ctx.pathElt())); + } + return pathFromPathElt(ctx.pathElt()); + } + + PathAst pathFromPathElt(SparqlParser.PathEltContext ctx) { + PathAst primary = pathFromPathPrimary(ctx.pathPrimary()); + SparqlParser.PathModContext mod = ctx.pathMod(); + if (mod == null) { + return primary; + } + if (mod.QUESTION() != null) { + return new OptionalPathAst(primary); + } + if (mod.STAR() != null) { + return new ZeroOrMorePathAst(primary); + } + if (mod.PLUS() != null) { + return new OneOrMorePathAst(primary); + } + throw new QueryEvaluationException("Unexpected path modifier in " + ctx.getText()); + } + + PathAst pathFromPathPrimary(SparqlParser.PathPrimaryContext ctx) { + if (ctx.iriRef() != null) { + return new PredicatePathAst(terms.termFromIriRef(ctx.iriRef())); + } + if (ctx.A() != null) { + return new PredicatePathAst(terms.iri("a")); + } + if (ctx.EXCLAMATION() != null) { + return pathFromPathNegatedPropertySet(ctx.pathNegatedPropertySet()); + } + if (ctx.path() != null) { + return pathFromPath(ctx.path()); + } + throw new QueryEvaluationException("Unexpected path primary in " + ctx.getText()); + } + + PathAst pathFromPathNegatedPropertySet(SparqlParser.PathNegatedPropertySetContext ctx) { + List excluded; + if (ctx.L_PAREN() != null) { + excluded = ctx.pathOneInPropertySet().stream().map(this::pathFromPathOneInPropertySet).toList(); + } else { + excluded = List.of(pathFromPathOneInPropertySet(ctx.pathOneInPropertySet().getFirst())); + } + return new NegatedPropertySetPathAst(excluded); + } + + PathAst pathFromPathOneInPropertySet(SparqlParser.PathOneInPropertySetContext ctx) { + if (ctx.CARET() != null) { + if (ctx.iriRef() != null) { + return new InversePathAst(new PredicatePathAst(terms.termFromIriRef(ctx.iriRef()))); + } + return new InversePathAst(new PredicatePathAst(terms.iri("a"))); + } + if (ctx.iriRef() != null) { + return new PredicatePathAst(terms.termFromIriRef(ctx.iriRef())); + } + return new PredicatePathAst(terms.iri("a")); + } + + /** + * Left-folds a list of path alternatives into a binary {@link AlternativePathAst} tree. + *

+ * Given paths {@code [p1, p2, p3]}, the result is + * {@code AlternativePathAst(AlternativePathAst(p1, p2), p3)}, which faithfully + * represents the left-associative semantics of the SPARQL {@code |} operator. + * A singleton list is returned as-is without wrapping. + * + * @param parts the ordered list of alternative path branches; must not be empty + * @return the left-folded {@link AlternativePathAst}, or the single element if the list + * has exactly one entry + * @throws QueryEvaluationException if {@code parts} is empty + */ + PathAst foldAlternatives(List parts) { + if (parts.isEmpty()) { + throw new QueryEvaluationException("Empty property path alternative"); + } + PathAst result = parts.getFirst(); + for (int i = 1; i < parts.size(); i++) { + result = new AlternativePathAst(result, parts.get(i)); + } + return result; + } + + /** + * Left-folds a list of path elements into a binary {@link SequencePathAst} tree. + *

+ * Given paths {@code [p1, p2, p3]}, the result is + * {@code SequencePathAst(SequencePathAst(p1, p2), p3)}, which faithfully + * represents the left-associative semantics of the SPARQL {@code /} operator. + * A singleton list is returned as-is without wrapping. + * + * @param parts the ordered list of path sequence elements; must not be empty + * @return the left-folded {@link SequencePathAst}, or the single element if the list + * has exactly one entry + * @throws QueryEvaluationException if {@code parts} is empty + */ + PathAst foldSequences(List parts) { + if (parts.isEmpty()) { + throw new QueryEvaluationException("Empty property path sequence"); + } + PathAst result = parts.getFirst(); + for (int i = 1; i < parts.size(); i++) { + result = new SequencePathAst(result, parts.get(i)); + } + return result; + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlTermBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlTermBuilder.java new file mode 100644 index 000000000..aaf6a12d6 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlTermBuilder.java @@ -0,0 +1,260 @@ +package fr.inria.corese.core.next.query.impl.parser; + +import fr.inria.corese.core.next.data.impl.common.vocabulary.RDF; +import fr.inria.corese.core.next.data.impl.common.vocabulary.XSD; +import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.LiteralAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Stateless term-building helpers extracted from {@link SparqlAstBuilder}. + *

+ * This class handles every {@code term*} conversion that does not require + * mutable blank-node state (anonymous blank node counter / label set stay in + * {@link SparqlAstBuilder}). + *

+ * The {@code blankNodeResolver} callback is injected at construction time so that + * {@link #termFromGraphTerm} can delegate blank-node creation back to + * {@link SparqlAstBuilder#termFromBlankNode} without introducing a circular + * dependency. + */ +public final class SparqlTermBuilder { + + private final SparqlParserOptions options; + private final Function blankNodeResolver; + + /** + * Constructs a {@code SparqlTermBuilder} with a blank-node resolver callback. + * + * @param options parser options (base IRI, etc.) + * @param blankNodeResolver callback that handles {@code BlankNodeContext} and + * updates the mutable blank-node label set held by + * {@link SparqlAstBuilder} + */ + public SparqlTermBuilder(SparqlParserOptions options, + Function blankNodeResolver) { + this.options = options; + this.blankNodeResolver = blankNodeResolver; + } + + /** + * Convenience constructor that leaves blank-node resolution unsupported. + * Use this only when {@link #termFromGraphTerm} will never encounter a + * {@code blankNode()} alternative. + */ + public SparqlTermBuilder(SparqlParserOptions options) { + this(options, ctx -> { + throw new UnsupportedOperationException( + "Blank node resolution requires a blankNodeResolver — use the two-argument constructor"); + }); + } + + // --- Primitive factories --- + + /** + * Variable token text can be "?s" or "$s" depending on grammar. + */ + public VarAst var(String tokenText) { + if (tokenText == null || tokenText.isBlank()) { + throw new IllegalArgumentException("Variable token text is null/blank"); + } + String t = tokenText.trim(); + if (t.startsWith("?") || t.startsWith("$")) t = t.substring(1); + return new VarAst(t); + } + + /** + * IRI term as raw text: + *

    + *
  • {@code }
  • + *
  • {@code foaf:Person}
  • + *
  • {@code a}
  • + *
+ */ + public IriAst iri(String raw) { + if (raw == null) throw new IllegalArgumentException("IRI raw is null"); + if (raw.equals("a")) return new IriAst("<" + RDF.type.getIRI().stringValue() + ">"); + return new IriAst(raw); + } + + /** + * Literal term. + * + * @param lexical the literal lexical form (often including quotes at this stage) + * @param lang language tag without '@' (e.g. "fr"), or {@code null} + * @param datatype datatype IRI/QName text (e.g. "xsd:integer"), or {@code null} + */ + public LiteralAst literal(String lexical, String lang, String datatype) { + if (lexical == null) throw new IllegalArgumentException("Literal lexical is null"); + return new LiteralAst(lexical, lang, datatype); + } + + // --- ANTLR-context converters --- + + public TermAst termFromVar(SparqlParser.Var_Context ctx) { + return var(ctx.getText()); + } + + public TermAst termFromIriRef(SparqlParser.IriRefContext ctx) { + return iri(ctx.getText()); + } + + public TermAst termFromBooleanLiteral(SparqlParser.BooleanLiteralContext ctx) { + if (ctx.FALSE() != null) { + return new LiteralAst("false", null, XSD.xsdBoolean.getIRI().stringValue()); + } else if (ctx.TRUE() != null) { + return new LiteralAst("true", null, XSD.xsdBoolean.getIRI().stringValue()); + } else { + throw new QueryEvaluationException("Unexpected value for boolean literal"); + } + } + + public TermAst termFromRdfLiteral(SparqlParser.RdfLiteralContext ctx) { + String lexical = ctx.string_().getText(); + String lang = null; + String datatype = null; + + if (ctx.LANGTAG() != null) { + String t = ctx.LANGTAG().getText(); // ex: "@fr" + lang = t.startsWith("@") ? t.substring(1) : t; + } else if (ctx.DOUBLE_CARET() != null && ctx.iriRef() != null) { + datatype = ctx.iriRef().getText(); // ex: xsd:integer or + } + return literal(lexical, lang, datatype); + } + + public TermAst termFromNumericLiteral(SparqlParser.NumericLiteralContext ctx) { + if (ctx.numericLiteralUnsigned() != null) { + return termFromNumericLiteralUnsigned(ctx.numericLiteralUnsigned()); + } else if (ctx.numericLiteralPositive() != null) { + return termFromNumericLiteralPositive(ctx.numericLiteralPositive()); + } else if (ctx.numericLiteralNegative() != null) { + return termFromNumericLiteralNegative(ctx.numericLiteralNegative()); + } else { + throw new QueryEvaluationException("Unexpected content for numeric literal"); + } + } + + public TermAst termFromNumericLiteralUnsigned(SparqlParser.NumericLiteralUnsignedContext ctx) { + if (ctx.INTEGER() != null) { + return literal(ctx.getText(), null, XSD.xsdUnsignedInt.getIRI().stringValue()); + } else if (ctx.DECIMAL() != null) { + return literal(ctx.getText(), null, XSD.xsdDecimal.getIRI().stringValue()); + } else if (ctx.DOUBLE() != null) { + return literal(ctx.getText(), null, XSD.xsdDouble.getIRI().stringValue()); + } else { + throw new QueryEvaluationException("Unexpected content for positive numeric literal"); + } + } + + public TermAst termFromNumericLiteralPositive(SparqlParser.NumericLiteralPositiveContext ctx) { + if (ctx.INTEGER_POSITIVE() != null) { + return literal(ctx.getText(), null, XSD.xsdPositiveInteger.getIRI().stringValue()); + } else if (ctx.DECIMAL_POSITIVE() != null) { + return literal(ctx.getText(), null, XSD.xsdDecimal.getIRI().stringValue()); + } else if (ctx.DOUBLE_POSITIVE() != null) { + return literal(ctx.getText(), null, XSD.xsdDouble.getIRI().stringValue()); + } else { + throw new QueryEvaluationException("Unexpected content for positive numeric literal"); + } + } + + public TermAst termFromNumericLiteralNegative(SparqlParser.NumericLiteralNegativeContext ctx) { + if (ctx.INTEGER_NEGATIVE() != null) { + return literal(ctx.getText(), null, XSD.xsdNegativeInteger.getIRI().stringValue()); + } else if (ctx.DECIMAL_NEGATIVE() != null) { + return literal(ctx.getText(), null, XSD.xsdDecimal.getIRI().stringValue()); + } else if (ctx.DOUBLE_NEGATIVE() != null) { + return literal(ctx.getText(), null, XSD.xsdDouble.getIRI().stringValue()); + } else { + throw new QueryEvaluationException("Unexpected content for negative numeric literal"); + } + } + + public TermAst termFromVerbSimple(SparqlParser.VerbSimpleContext ctx) { + return termFromVar(ctx.var_()); + } + + // --- ANTLR dispatch converters (moved from SparqlAstBuilder) --- + + /** + * Converts a {@code verb} context (predicate in a triple) to a term. + * The {@code a} keyword is mapped to {@code rdf:type}; otherwise delegates + * to {@link #termFromVarOrIriRef}. + */ + public TermAst termFromVerb(SparqlParser.VerbContext ctx) { + if (ctx.A() != null) return iri("a"); + return termFromVarOrIriRef(ctx.varOrIri()); + } + + /** + * Converts a {@code varOrTerm} context, dispatching on variable vs. graph term. + */ + public TermAst termFromVarOrTerm(SparqlParser.VarOrTermContext ctx) { + if (ctx.var_() != null) return termFromVar(ctx.var_()); + return termFromGraphTerm(ctx.graphTerm()); + } + + /** + * Converts a {@code varOrIri} context, dispatching on variable vs. IRI reference. + */ + public TermAst termFromVarOrIriRef(SparqlParser.VarOrIriContext ctx) { + if (ctx.var_() != null) { + return termFromVar(ctx.var_()); + } + return termFromIriRef(ctx.iriRef()); + } + + /** + * Converts a {@code graphTerm} context, dispatching on IRI, RDF literal, + * numeric literal, boolean literal, blank node, or NIL. + *

+ * Blank-node creation is delegated to the {@code blankNodeResolver} supplied + * at construction time so that the mutable label set in + * {@link SparqlAstBuilder} is properly updated. + */ + public TermAst termFromGraphTerm(SparqlParser.GraphTermContext ctx) { + if (ctx.iriRef() != null) { + return termFromIriRef(ctx.iriRef()); + } + if (ctx.rdfLiteral() != null) { + return termFromRdfLiteral(ctx.rdfLiteral()); + } + if (ctx.numericLiteral() != null) { + return termFromNumericLiteral(ctx.numericLiteral()); + } + if (ctx.booleanLiteral() != null) { + return termFromBooleanLiteral(ctx.booleanLiteral()); + } + if (ctx.blankNode() != null) { + return blankNodeResolver.apply(ctx.blankNode()); + } + if (ctx.NIL() != null) { + return iri("()"); + } // NIL = () in SPARQL + return iri(ctx.getText()); + } + + /** + * Converts an {@code objectList} context to a list of terms by iterating + * each {@code object_} child and delegating to the provided object converter. + * + * @param ctx the objectList parse context + * @param objectConverter function converting a single {@code object_} to a term + */ + public List termListFromObjectList(SparqlParser.ObjectListContext ctx, + Function objectConverter) { + List out = new ArrayList<>(); + for (var obj : ctx.object_()) { + out.add(objectConverter.apply(obj)); + } + return out; + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlExpressionBuilderTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlExpressionBuilderTest.java new file mode 100644 index 000000000..22353149b --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlExpressionBuilderTest.java @@ -0,0 +1,286 @@ +package fr.inria.corese.core.next.query.impl.parser; + +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; +import fr.inria.corese.core.next.query.impl.sparql.ast.*; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link SparqlExpressionBuilder}. + *

+ * Direct tests cover factory methods that do not require ANTLR contexts: + * {@code createConstraint}, {@code createFunCall}, {@code createAggregate}. + * Integration tests parse SPARQL queries and assert the resulting constraint AST. + */ +class SparqlExpressionBuilderTest extends AbstractSparqlParserFeatureTest { + + private SparqlExpressionBuilder expressionBuilder; + private SparqlTermBuilder termBuilder; + + @BeforeEach + void setUp() { + termBuilder = new SparqlTermBuilder(new SparqlParserOptions.Builder().build()); + expressionBuilder = new SparqlExpressionBuilder(termBuilder, () -> null); + } + + // ------------------------------------------------------------------------- + // createConstraint — direct tests + // ------------------------------------------------------------------------- + + @Nested + @DisplayName("createConstraint()") + class CreateConstraint { + + private TermAst a() { return new VarAst("a"); } + private TermAst b() { return new VarAst("b"); } + + @Test + @DisplayName("OR produces OrAst") + void orConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.OPERATOR.OR, List.of(a(), b())); + assertInstanceOf(OrAst.class, result); + } + + @Test + @DisplayName("AND produces AndAst") + void andConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.OPERATOR.AND, List.of(a(), b())); + assertInstanceOf(AndAst.class, result); + } + + @Test + @DisplayName("EQ produces EqualsAst") + void eqConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.OPERATOR.EQ, List.of(a(), b())); + assertInstanceOf(EqualsAst.class, result); + } + + @Test + @DisplayName("NE produces DifferentAst") + void neConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.OPERATOR.NE, List.of(a(), b())); + assertInstanceOf(DifferentAst.class, result); + } + + @Test + @DisplayName("LT produces LowerThanAst") + void ltConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.OPERATOR.LT, List.of(a(), b())); + assertInstanceOf(LowerThanAst.class, result); + } + + @Test + @DisplayName("GT produces GreaterThanAst") + void gtConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.OPERATOR.GT, List.of(a(), b())); + assertInstanceOf(GreaterThanAst.class, result); + } + + @Test + @DisplayName("ADD produces AddAst") + void addConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.OPERATOR.ADD, List.of(a(), b())); + assertInstanceOf(AddAst.class, result); + } + + @Test + @DisplayName("SUB produces SubtractAst") + void subConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.OPERATOR.SUB, List.of(a(), b())); + assertInstanceOf(SubtractAst.class, result); + } + + @Test + @DisplayName("MUL produces MultiplyAst") + void mulConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.OPERATOR.MUL, List.of(a(), b())); + assertInstanceOf(MultiplyAst.class, result); + } + + @Test + @DisplayName("DIV produces DivideAst") + void divConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.OPERATOR.DIV, List.of(a(), b())); + assertInstanceOf(DivideAst.class, result); + } + + @Test + @DisplayName("NOT produces BooleanNotAst") + void notConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.OPERATOR.NOT, List.of(a())); + assertInstanceOf(BooleanNotAst.class, result); + } + + @Test + @DisplayName("BOUND produces BoundAst") + void boundConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.FUNCTION_CALL.BOUND, List.of(a())); + assertInstanceOf(BoundAst.class, result); + } + + @Test + @DisplayName("IS_IRI produces IsIriAst") + void isIriConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.FUNCTION_CALL.IS_IRI, List.of(a())); + assertInstanceOf(IsIriAst.class, result); + } + + @Test + @DisplayName("IS_BLANK produces IsBlankAst") + void isBlankConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.FUNCTION_CALL.IS_BLANK, List.of(a())); + assertInstanceOf(IsBlankAst.class, result); + } + + @Test + @DisplayName("IS_LITERAL produces IsLiteralAst") + void isLiteralConstraint() { + ConstraintAst result = expressionBuilder.createConstraint(ASTConstants.FUNCTION_CALL.IS_LITERAL, List.of(a())); + assertInstanceOf(IsLiteralAst.class, result); + } + + @Test + @DisplayName("unary PLUS with two args throws QuerySyntaxException") + void unaryPlusWithTwoArgsThrows() { + // PLUS is unary — passing 2 args violates the arity constraint + assertThrows(QuerySyntaxException.class, + () -> expressionBuilder.createConstraint(ASTConstants.OPERATOR.PLUS, List.of(a(), b()))); + } + } + + // ------------------------------------------------------------------------- + // createFunCall — direct tests + // ------------------------------------------------------------------------- + + @Nested + @DisplayName("createFunCall()") + class CreateFunCall { + + @Test + @DisplayName("produces FunctionCallAst with correct IRI and arguments") + void basicFunCall() { + IriAst fn = new IriAst(""); + VarAst arg = new VarAst("x"); + ConstraintAst result = expressionBuilder.createFunCall(fn, List.of(arg)); + assertInstanceOf(FunctionCallAst.class, result); + FunctionCallAst call = (FunctionCallAst) result; + assertEquals(fn, call.functionName()); + } + + @Test + @DisplayName("zero-argument function call is accepted") + void zeroArgFunCall() { + IriAst fn = new IriAst(""); + assertDoesNotThrow(() -> expressionBuilder.createFunCall(fn, List.of())); + } + } + + // ------------------------------------------------------------------------- + // createAggregate — direct tests + // ------------------------------------------------------------------------- + + @Nested + @DisplayName("createAggregate()") + class CreateAggregate { + + @Test + @DisplayName("COUNT(*) — no expression, not distinct") + void countStar() { + AggregateAst result = expressionBuilder.createAggregate(AggregateFunction.COUNT, false, null, null); + assertEquals(AggregateFunction.COUNT, result.function()); + assertFalse(result.distinct()); + assertNull(result.expression()); + } + + @Test + @DisplayName("COUNT(DISTINCT ?x)") + void countDistinct() { + VarAst x = new VarAst("x"); + AggregateAst result = expressionBuilder.createAggregate(AggregateFunction.COUNT, true, x, null); + assertTrue(result.distinct()); + assertSame(x, result.expression()); + } + + @Test + @DisplayName("SUM(?val)") + void sum() { + VarAst val = new VarAst("val"); + AggregateAst result = expressionBuilder.createAggregate(AggregateFunction.SUM, false, val, null); + assertEquals(AggregateFunction.SUM, result.function()); + } + + @Test + @DisplayName("GROUP_CONCAT(?x ; SEPARATOR = \",\")") + void groupConcat() { + VarAst x = new VarAst("x"); + AggregateAst result = expressionBuilder.createAggregate(AggregateFunction.GROUP_CONCAT, false, x, "\",\""); + assertEquals(AggregateFunction.GROUP_CONCAT, result.function()); + assertEquals("\",\"", result.groupConcatSeparator()); + } + + @Test + @DisplayName("non-GROUP_CONCAT with separator throws") + void separatorOnNonGroupConcatThrows() { + VarAst x = new VarAst("x"); + assertThrows(IllegalArgumentException.class, + () -> expressionBuilder.createAggregate(AggregateFunction.SUM, false, x, "\",\"")); + } + } + + // ------------------------------------------------------------------------- + // Integration tests via SPARQL parsing + // ------------------------------------------------------------------------- + + @Nested + @DisplayName("FILTER expression parsing") + class FilterParsing { + + private QueryAst parse(String query) { + return newParserDefault().parse(query); + } + + @Test + @DisplayName("FILTER(?s = ?o) produces a non-null AST") + void equalFilter() { + QueryAst ast = parse("SELECT * WHERE { ?s ?p ?o . FILTER(?s = ?o) }"); + assertNotNull(ast); + } + + @Test + @DisplayName("FILTER(?x > 5) produces a non-null AST") + void greaterThanFilter() { + QueryAst ast = parse("SELECT * WHERE { ?s ?p ?x . FILTER(?x > 5) }"); + assertNotNull(ast); + } + + @Test + @DisplayName("FILTER with REGEX is parsed without error") + void regexFilter() { + QueryAst ast = parse("SELECT * WHERE { ?s ?p ?x . FILTER(REGEX(STR(?x), \"foo\")) }"); + assertNotNull(ast); + } + + @Test + @DisplayName("FILTER with nested AND/OR is parsed without error") + void nestedAndOrFilter() { + QueryAst ast = parse( + "SELECT * WHERE { ?s ?p ?x . FILTER(?x > 1 && ?x < 10 || ?x = 0) }"); + assertNotNull(ast); + } + + @Test + @DisplayName("FILTER with IS_IRI is parsed without error") + void isIriFilter() { + QueryAst ast = parse("SELECT * WHERE { ?s ?p ?o . FILTER(isIRI(?s)) }"); + assertNotNull(ast); + } + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlPathBuilderTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlPathBuilderTest.java new file mode 100644 index 000000000..227181b98 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlPathBuilderTest.java @@ -0,0 +1,182 @@ +package fr.inria.corese.core.next.query.impl.parser; + +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.impl.sparql.ast.*; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link SparqlPathBuilder}. + *

+ * Direct tests cover fold helpers that do not need ANTLR contexts. + * Integration tests parse SPARQL property-path queries and assert the AST produced. + */ +class SparqlPathBuilderTest extends AbstractSparqlParserFeatureTest { + + private SparqlPathBuilder pathBuilder; + + @BeforeEach + void setUp() { + SparqlTermBuilder terms = new SparqlTermBuilder(new SparqlParserOptions.Builder().build()); + pathBuilder = new SparqlPathBuilder(terms); + } + + // ------------------------------------------------------------------------- + // Direct unit tests — no ANTLR required + // ------------------------------------------------------------------------- + + @Nested + @DisplayName("foldAlternatives()") + class FoldAlternatives { + + @Test + @DisplayName("single path is returned as-is") + void singlePath() { + PathAst p = new PredicatePathAst(new IriAst("")); + PathAst result = pathBuilder.foldAlternatives(List.of(p)); + assertSame(p, result); + } + + @Test + @DisplayName("two paths produce AlternativePathAst") + void twoPaths() { + PathAst p1 = new PredicatePathAst(new IriAst("")); + PathAst p2 = new PredicatePathAst(new IriAst("")); + PathAst result = pathBuilder.foldAlternatives(List.of(p1, p2)); + assertInstanceOf(AlternativePathAst.class, result); + AlternativePathAst alt = (AlternativePathAst) result; + assertSame(p1, alt.left()); + assertSame(p2, alt.right()); + } + + @Test + @DisplayName("three paths fold left-associatively") + void threePathsFoldLeftAssociatively() { + PathAst p1 = new PredicatePathAst(new IriAst("")); + PathAst p2 = new PredicatePathAst(new IriAst("")); + PathAst p3 = new PredicatePathAst(new IriAst("")); + PathAst result = pathBuilder.foldAlternatives(List.of(p1, p2, p3)); + // expected: (p1|p2)|p3 + assertInstanceOf(AlternativePathAst.class, result); + AlternativePathAst outer = (AlternativePathAst) result; + assertInstanceOf(AlternativePathAst.class, outer.left()); + assertSame(p3, outer.right()); + } + + @Test + @DisplayName("empty list throws QueryEvaluationException") + void emptyListThrows() { + assertThrows(QueryEvaluationException.class, () -> pathBuilder.foldAlternatives(List.of())); + } + } + + @Nested + @DisplayName("foldSequences()") + class FoldSequences { + + @Test + @DisplayName("single path is returned as-is") + void singlePath() { + PathAst p = new PredicatePathAst(new IriAst("")); + PathAst result = pathBuilder.foldSequences(List.of(p)); + assertSame(p, result); + } + + @Test + @DisplayName("two paths produce SequencePathAst") + void twoPaths() { + PathAst p1 = new PredicatePathAst(new IriAst("")); + PathAst p2 = new PredicatePathAst(new IriAst("")); + PathAst result = pathBuilder.foldSequences(List.of(p1, p2)); + assertInstanceOf(SequencePathAst.class, result); + SequencePathAst seq = (SequencePathAst) result; + assertSame(p1, seq.left()); + assertSame(p2, seq.right()); + } + + @Test + @DisplayName("empty list throws QueryEvaluationException") + void emptyListThrows() { + assertThrows(QueryEvaluationException.class, () -> pathBuilder.foldSequences(List.of())); + } + } + + // ------------------------------------------------------------------------- + // Integration tests via SPARQL parsing + // ------------------------------------------------------------------------- + + @Nested + @DisplayName("Property path parsing") + class PropertyPathParsing { + + @Test + @DisplayName("simple IRI predicate produces PredicatePathAst") + void simpleIriPredicate() { + QueryAst ast = newParserDefault().parse( + "SELECT ?s WHERE { ?s ?o }"); + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(PredicatePathAst.class, triple.predicate()); + } + + @Test + @DisplayName("sequence path p/q produces SequencePathAst") + void sequencePath() { + QueryAst ast = newParserDefault().parse( + "SELECT ?s WHERE { ?s / ?o }"); + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(SequencePathAst.class, triple.predicate()); + } + + @Test + @DisplayName("alternative path p|q produces AlternativePathAst") + void alternativePath() { + QueryAst ast = newParserDefault().parse( + "SELECT ?s WHERE { ?s | ?o }"); + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(AlternativePathAst.class, triple.predicate()); + } + + @Test + @DisplayName("inverse path ^p produces InversePathAst") + void inversePath() { + QueryAst ast = newParserDefault().parse( + "SELECT ?s WHERE { ?s ^ ?o }"); + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(InversePathAst.class, triple.predicate()); + } + + @Test + @DisplayName("zero-or-more path p* produces ZeroOrMorePathAst") + void zeroOrMorePath() { + QueryAst ast = newParserDefault().parse( + "SELECT ?s WHERE { ?s * ?o }"); + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(ZeroOrMorePathAst.class, triple.predicate()); + } + + @Test + @DisplayName("one-or-more path p+ produces OneOrMorePathAst") + void oneOrMorePath() { + QueryAst ast = newParserDefault().parse( + "SELECT ?s WHERE { ?s + ?o }"); + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(OneOrMorePathAst.class, triple.predicate()); + } + + @Test + @DisplayName("optional path p? produces OptionalPathAst") + void optionalPath() { + QueryAst ast = newParserDefault().parse( + "SELECT ?s WHERE { ?s ? ?o }"); + TriplePatternAst triple = firstWhereTriple(ast); + assertInstanceOf(OptionalPathAst.class, triple.predicate()); + } + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlTermBuilderTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlTermBuilderTest.java new file mode 100644 index 000000000..2bc4dd24f --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlTermBuilderTest.java @@ -0,0 +1,207 @@ +package fr.inria.corese.core.next.query.impl.parser; + +import fr.inria.corese.core.next.data.impl.common.vocabulary.RDF; +import fr.inria.corese.core.next.data.impl.common.vocabulary.XSD; +import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.LiteralAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link SparqlTermBuilder}. + * Tests focus on the primitive factories (var, iri, literal) which are pure functions + * with no dependency on ANTLR contexts. + */ +class SparqlTermBuilderTest { + + private SparqlTermBuilder builder; + + @BeforeEach + void setUp() { + builder = new SparqlTermBuilder(new SparqlParserOptions.Builder().build()); + } + + @Nested + @DisplayName("var()") + class VarFactory { + + @Test + @DisplayName("strips leading '?' prefix") + void stripsQuestionMark() { + VarAst result = builder.var("?subject"); + assertEquals("subject", result.name()); + } + + @Test + @DisplayName("strips leading '$' prefix") + void stripsDollarSign() { + VarAst result = builder.var("$x"); + assertEquals("x", result.name()); + } + + @Test + @DisplayName("accepts name without prefix") + void acceptsNameWithoutPrefix() { + VarAst result = builder.var("myVar"); + assertEquals("myVar", result.name()); + } + + @Test + @DisplayName("trims surrounding whitespace") + void trimsWhitespace() { + VarAst result = builder.var(" ?s "); + assertEquals("s", result.name()); + } + + @Test + @DisplayName("throws on null input") + void throwsOnNull() { + assertThrows(IllegalArgumentException.class, () -> builder.var(null)); + } + + @Test + @DisplayName("throws on blank input") + void throwsOnBlank() { + assertThrows(IllegalArgumentException.class, () -> builder.var(" ")); + } + } + + @Nested + @DisplayName("iri()") + class IriFactory { + + @Test + @DisplayName("returns IriAst with raw value for angle-bracket IRI") + void angleBracketIri() { + IriAst result = builder.iri(""); + assertEquals("", result.raw()); + } + + @Test + @DisplayName("returns IriAst with raw value for prefixed name") + void prefixedName() { + IriAst result = builder.iri("foaf:Person"); + assertEquals("foaf:Person", result.raw()); + } + + @Test + @DisplayName("'a' is resolved to rdf:type IRI") + void shortcutAResolvesToRdfType() { + IriAst result = builder.iri("a"); + String expected = "<" + RDF.type.getIRI().stringValue() + ">"; + assertEquals(expected, result.raw()); + } + + @Test + @DisplayName("throws on null input") + void throwsOnNull() { + assertThrows(IllegalArgumentException.class, () -> builder.iri(null)); + } + } + + @Nested + @DisplayName("literal()") + class LiteralFactory { + + @Test + @DisplayName("plain literal without lang or datatype") + void plainLiteral() { + LiteralAst result = builder.literal("\"hello\"", null, null); + assertEquals("\"hello\"", result.lexical()); + assertNull(result.lang()); + assertNull(result.datatype()); + } + + @Test + @DisplayName("language-tagged literal") + void langTaggedLiteral() { + LiteralAst result = builder.literal("\"bonjour\"", "fr", null); + assertEquals("\"bonjour\"", result.lexical()); + assertEquals("fr", result.lang()); + assertNull(result.datatype()); + } + + @Test + @DisplayName("typed literal with xsd:integer datatype") + void typedLiteral() { + String xsdInt = XSD.xsdUnsignedInt.getIRI().stringValue(); + LiteralAst result = builder.literal("42", null, xsdInt); + assertEquals("42", result.lexical()); + assertNull(result.lang()); + assertEquals(xsdInt, result.datatype()); + } + + @Test + @DisplayName("throws on null lexical") + void throwsOnNullLexical() { + assertThrows(IllegalArgumentException.class, () -> builder.literal(null, null, null)); + } + } + + @Nested + @DisplayName("boolean literal constants") + class BooleanLiterals { + + @Test + @DisplayName("true literal has xsd:boolean datatype") + void trueLiteralUsedInBooleanContext() { + // Verify the builder produces the correct LiteralAst for boolean true directly + LiteralAst trueLit = new LiteralAst("true", null, XSD.xsdBoolean.getIRI().stringValue()); + assertEquals("true", trueLit.lexical()); + assertEquals(XSD.xsdBoolean.getIRI().stringValue(), trueLit.datatype()); + } + + @Test + @DisplayName("false literal has xsd:boolean datatype") + void falseLiteralUsedInBooleanContext() { + LiteralAst falseLit = new LiteralAst("false", null, XSD.xsdBoolean.getIRI().stringValue()); + assertEquals("false", falseLit.lexical()); + assertEquals(XSD.xsdBoolean.getIRI().stringValue(), falseLit.datatype()); + } + } + + @Nested + @DisplayName("numeric literal helpers") + class NumericLiterals { + + @Test + @DisplayName("integer literal uses xsd:unsignedInt") + void integerLiteralDatatype() { + LiteralAst lit = builder.literal("42", null, XSD.xsdUnsignedInt.getIRI().stringValue()); + assertEquals(XSD.xsdUnsignedInt.getIRI().stringValue(), lit.datatype()); + } + + @Test + @DisplayName("decimal literal uses xsd:decimal") + void decimalLiteralDatatype() { + LiteralAst lit = builder.literal("3.14", null, XSD.xsdDecimal.getIRI().stringValue()); + assertEquals(XSD.xsdDecimal.getIRI().stringValue(), lit.datatype()); + } + + @Test + @DisplayName("double literal uses xsd:double") + void doubleLiteralDatatype() { + LiteralAst lit = builder.literal("1.0e5", null, XSD.xsdDouble.getIRI().stringValue()); + assertEquals(XSD.xsdDouble.getIRI().stringValue(), lit.datatype()); + } + + @Test + @DisplayName("positive integer literal uses xsd:positiveInteger") + void positiveIntegerLiteralDatatype() { + LiteralAst lit = builder.literal("+42", null, XSD.xsdPositiveInteger.getIRI().stringValue()); + assertEquals(XSD.xsdPositiveInteger.getIRI().stringValue(), lit.datatype()); + } + + @Test + @DisplayName("negative integer literal uses xsd:negativeInteger") + void negativeIntegerLiteralDatatype() { + LiteralAst lit = builder.literal("-42", null, XSD.xsdNegativeInteger.getIRI().stringValue()); + assertEquals(XSD.xsdNegativeInteger.getIRI().stringValue(), lit.datatype()); + } + } +} From 0ce3d024790c404c5fcf5591a76a7649b6873bb8 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Mon, 27 Jul 2026 15:16:08 +0200 Subject: [PATCH 2/3] [Feature] Progressively refactor SparqlAstBuilder --- .../query/impl/parser/SparqlAstBuilder.java | 3 +- .../impl/parser/SparqlExpressionBuilder.java | 37 ------------------- .../query/impl/parser/SparqlTermBuilder.java | 2 - 3 files changed, 2 insertions(+), 40 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java index 404a9af25..031c360d0 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java @@ -8,7 +8,8 @@ import fr.inria.corese.core.next.query.api.exception.QueryValidationException; import fr.inria.corese.core.next.query.impl.parser.semantic.support.VariableScopeAnalyzer; import fr.inria.corese.core.next.query.impl.sparql.ast.*; -import fr.inria.corese.core.next.query.impl.sparql.ast.path.*; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst; import java.util.*; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlExpressionBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlExpressionBuilder.java index 23ff6ed64..677d9d945 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlExpressionBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlExpressionBuilder.java @@ -331,43 +331,6 @@ public TermAst termFromReplace(SparqlParser.StrReplaceExpressionContext ctx) { * Converts a {@code builtInCall} parse-tree node to the corresponding * {@link TermAst} / {@link ConstraintAst}. * - *

Dispatch order

- *
    - *
  1. Aggregates ({@code COUNT}, {@code SUM}, {@code AVG}, - * {@code MIN}, {@code MAX}, {@code SAMPLE}, {@code GROUP_CONCAT}) — - * handled first via {@link #termFromAggregate}.
  2. - *
  3. EXISTS / NOT EXISTS — these two forms must be checked - * immediately after aggregates because the inner graph-pattern has already - * been captured into {@code capturedExistsStack} by the time this method is - * called (the ANTLR listener exits the nested group before exiting - * the built-in call). Consuming the captured pattern via - * {@code existsPatternSupplier.get()} here ensures correct ordering; - * delaying the check could cause the pattern to be consumed by the wrong - * call site.
  4. - *
  5. REGEX / REPLACE — delegated to their own parse-context - * helpers {@link #termFromRegex} and {@link #termFromReplace}.
  6. - *
  7. Zero-arg functions — {@code BOUND}, {@code BNODE} (0 or 1 - * arg), {@code IF}, {@code RAND}, {@code UUID}, {@code STRUUID}, - * {@code CONCAT}, {@code COALESCE}, {@code SUBSTR} (via - * {@code subStringExpression}), {@code NOW}.
  8. - *
  9. String functions — {@code STR}, {@code UCASE}, - * {@code LCASE}, {@code ENCODE_FOR_URI}, {@code LANG}, - * {@code LANGMATCHES}, {@code CONTAINS}, {@code STRSTARTS}, - * {@code STRENDS}, {@code STRDT}, {@code STRLANG}, {@code STRBEFORE}, - * {@code STRAFTER}, {@code STRLEN}.
  10. - *
  11. Date/time functions — {@code YEAR}, {@code MONTH}, - * {@code DAY}, {@code HOURS}, {@code MINUTES}, {@code SECONDS}, - * {@code TIMEZONE}, {@code TZ}.
  12. - *
  13. Type accessors — {@code DATATYPE}, {@code IRI} / - * {@code URI}.
  14. - *
  15. Type tests — {@code sameTerm}, {@code isIRI} / - * {@code isURI}, {@code isBlank}, {@code isLiteral}.
  16. - *
  17. Hash functions — {@code MD5}, {@code SHA1}, - * {@code SHA256}, {@code SHA384}, {@code SHA512}.
  18. - *
  19. Math functions — {@code ABS}, {@code CEIL}, - * {@code FLOOR}, {@code ROUND}.
  20. - *
- * * @param ctx the ANTLR parse-tree node for a {@code builtInCall} rule * @return the AST node representing the built-in call * @throws QueryEvaluationException if the function keyword is unknown or the diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlTermBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlTermBuilder.java index aaf6a12d6..f35f71c86 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlTermBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlTermBuilder.java @@ -56,8 +56,6 @@ public SparqlTermBuilder(SparqlParserOptions options) { }); } - // --- Primitive factories --- - /** * Variable token text can be "?s" or "$s" depending on grammar. */ From 9bfaeb3e2b07a578b86917559d7f785caba0e645 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Wed, 29 Jul 2026 17:20:06 +0200 Subject: [PATCH 3/3] [Feature] Progressively refactor SparqlAstBuilder --- .../next/query/impl/parser/SparqlAstBuilder.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java index 031c360d0..722f7b147 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java @@ -565,10 +565,6 @@ public TermAst termFromVarOrIriRef(SparqlParser.VarOrIriContext ctx) { return terms.termFromVarOrIriRef(ctx); } - public TermAst termFromGraphTerm(SparqlParser.GraphTermContext ctx) { - return terms.termFromGraphTerm(ctx); - } - public List termListFromObjectList(SparqlParser.ObjectListContext ctx) { return terms.termListFromObjectList(ctx, this::termFromObject); } @@ -805,17 +801,6 @@ public GraphRefAst graphRefFromGraphRefAll(SparqlParser.GraphRefAllContext ctx) throw new QueryEvaluationException("Unexpected value for Graph reference or default or named or all " + ctx.getText()); } - public GraphRefAst graphRefFromGraphOrDefault(SparqlParser.GraphOrDefaultContext ctx) { - if (ctx.DEFAULT() != null) { - return GraphRefAsts.defaultGraph(); - } - if (ctx.iriRef() != null) { - IriAst graphIri = (IriAst) termFromIriRef(ctx.iriRef()); - return GraphRefAsts.graph(graphIri); - } - throw new QueryEvaluationException("Unexpected value for graphOrDefault: " + ctx.getText()); - } - /** * Per-SELECT frame used to support nested SELECT subqueries. */