diff --git a/src/main/java/fr/inria/corese/core/next/query/api/io/package-info.java b/src/main/java/fr/inria/corese/core/next/query/api/io/package-info.java
index 002fe4f98..d6f2afd68 100644
--- a/src/main/java/fr/inria/corese/core/next/query/api/io/package-info.java
+++ b/src/main/java/fr/inria/corese/core/next/query/api/io/package-info.java
@@ -5,8 +5,6 @@
* enumerates the supported serialization formats for SPARQL SELECT and ASK
* results (CSV, TSV, JSON, XML). Serializer contracts live in
* {@code io.serializer}; reusable option facets live in
- * {@code io.serializer.option}.
- * The recommended high-level entry point is {@link
- * fr.inria.corese.core.next.io.CoreseIO}.
+ * {@code io.serializer.option}.
*/
package fr.inria.corese.core.next.query.api.io;
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/Eval.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/Eval.java
index 31bbd885a..812e4c45f 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/Eval.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/Eval.java
@@ -23,7 +23,6 @@
import fr.inria.corese.core.next.query.api.exception.QueryTypeErrorException;
import fr.inria.corese.core.next.data.Values;
-import fr.inria.corese.core.next.query.impl.engine.event.ResultListener;
import fr.inria.corese.core.next.query.impl.engine.model.NodeImpl;
import fr.inria.corese.core.next.data.api.model.DatatypeValue;
import org.slf4j.Logger;
@@ -109,7 +108,6 @@ public Eval(Producer p, Evaluator e, Matcher m) {
evaluator = e;
match = m;
setVisitor(new ProcessVisitorDefault());
- e.setKGRAM(this);
graphEvaluator = new EvalGraph(this);
join = new EvalJoin(this);
optional = new EvalOptional(this);
@@ -724,9 +722,6 @@ int eval(Producer p, Node graphNode, Stack stack, Mappings map, int n) {
state.updateMaxLevel(n);
Exp exp = stack.get(n);
- if (visitors.hasListener()) {
- exp = visitors.listen(exp, n);
- }
// A false filter was detected at compile time, or this expression was
// identified as always failing: no use to evaluate it.
@@ -973,9 +968,6 @@ private int edge(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack
if (entity != null) {
state.incrementEdge();
- if (visitors.hasListener() && !visitors.listen(exp, qEdge, entity)) {
- continue;
- }
graph = entity.getGraph();
boolean bmatch = match(qEdge, entity, graphNode, graph, env);
@@ -1045,20 +1037,12 @@ Mappings selectQueryMappings(Mappings data) {
* Store a new result
*/
private int store(Producer p, Mapping m) {
- boolean store = true;
- if (getListener() != null) {
- store = getListener().process(getMemory());
- }
- if (store) {
- state.incrementResult();
- }
- if (store) {
- Mapping ans = m;
- if (m == null) {
- ans = getMemory().store(getQuery(), p, isSubEval);
- }
- store(ans);
+ state.incrementResult();
+ Mapping ans = m;
+ if (m == null) {
+ ans = getMemory().store(getQuery(), p, isSubEval);
}
+ store(ans);
return -1;
}
@@ -1149,14 +1133,6 @@ public boolean isJoinMappings() {
return joinMappings;
}
- public ResultListener getListener() {
- return visitors.getListener();
- }
-
- public void setListener(ResultListener listener) {
- visitors.setListener(listener);
- }
-
public EvaluationState getState() {
return state;
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalFilter.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalFilter.java
index 14e40df03..3126934cf 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalFilter.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalFilter.java
@@ -90,9 +90,6 @@ Mappings eval(Filter f, Environment env, Producer p, List nodes) {
int n = 1;
Expr exp = f.getExp();
if (exp.oper() == UNNEST) {
- if (engine.getVisitorDispatcher().hasListener()) {
- engine.getVisitorDispatcher().listen(exp);
- }
if (exp.arity() == 2) {
DatatypeValue dt = eval(exp.getExp(1), env, p);
if (dt == null) {
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalVisitorDispatcher.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalVisitorDispatcher.java
index d5a740670..5f6bcc42b 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalVisitorDispatcher.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalVisitorDispatcher.java
@@ -1,29 +1,19 @@
package fr.inria.corese.core.next.query.impl.engine.eval;
-import fr.inria.corese.core.next.data.api.model.DatatypeValue;
import fr.inria.corese.core.next.query.impl.engine.event.ProcessVisitor;
import fr.inria.corese.core.next.query.impl.engine.event.ProcessVisitorDefault;
-import fr.inria.corese.core.next.query.impl.engine.event.ResultListener;
-import fr.inria.corese.core.next.query.impl.engine.model.Edge;
-import fr.inria.corese.core.next.query.impl.engine.model.Expr;
-import fr.inria.corese.core.next.query.impl.engine.model.Node;
-import fr.inria.corese.core.next.query.impl.engine.pattern.Exp;
import java.util.Objects;
/**
- * Coordinates and dispatches execution lifecycle visitors and result listeners
- * for SPARQL evaluation in {@link Eval}.
+ * Coordinates and dispatches execution lifecycle visitors for SPARQL evaluation in {@link Eval}.
*
- *
Encapsulates the active {@link ProcessVisitor} (for query observation and reporting)
- * and the optional {@link ResultListener} (for inference rule execution).
+ *
Encapsulates the active {@link ProcessVisitor} for query observation and reporting.
*/
public final class EvalVisitorDispatcher {
- private ResultListener listener;
private ProcessVisitor visitor;
- private boolean hasListener = false;
private boolean hasStatement = false;
private boolean hasCandidate = false;
private boolean hasFilter = false;
@@ -44,15 +34,9 @@ public void setVisitor(ProcessVisitor visitor) {
}
public void updateVisitorFlags() {
- if (visitor != null) {
- this.hasStatement = visitor.statement();
- this.hasCandidate = visitor.candidate();
- this.hasFilter = visitor.filter();
- } else {
- this.hasStatement = false;
- this.hasCandidate = false;
- this.hasFilter = false;
- }
+ this.hasStatement = visitor.statement();
+ this.hasCandidate = visitor.candidate();
+ this.hasFilter = visitor.filter();
}
public boolean hasStatement() {
@@ -66,39 +50,4 @@ public boolean hasCandidate() {
public boolean hasFilter() {
return hasFilter;
}
-
- // --- Listener ---
-
- public ResultListener getListener() {
- return listener;
- }
-
- public void setListener(ResultListener listener) {
- this.listener = listener;
- this.hasListener = (listener != null);
- }
-
- public boolean hasListener() {
- return hasListener;
- }
-
- public Exp listen(Exp exp, int n) {
- if (hasListener) {
- return listener.listen(exp, n);
- }
- return exp;
- }
-
- public void listen(Expr exp) {
- if (hasListener) {
- listener.listen(exp);
- }
- }
-
- public boolean listen(Exp exp, Edge qEdge, Edge entity) {
- if (hasListener) {
- return listener.listen(exp, qEdge, entity);
- }
- return true;
- }
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/SparqlKgramEvaluator.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/SparqlKgramEvaluator.java
index 3414acf7b..ca1a31afe 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/SparqlKgramEvaluator.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/SparqlKgramEvaluator.java
@@ -18,20 +18,9 @@
public final class SparqlKgramEvaluator implements Evaluator {
private final OffsetDateTime queryEvaluationTime = OffsetDateTime.now(ZoneOffset.UTC);
- private Mode mode = Mode.KGRAM_MODE;
@Override
public OffsetDateTime getQueryEvaluationTime() {
return queryEvaluationTime;
}
-
- @Override
- public Mode getMode() {
- return mode;
- }
-
- @Override
- public void setMode(Mode mode) {
- this.mode = mode;
- }
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/ProcessVisitorDefault.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/ProcessVisitorDefault.java
index b792b1696..64a9bb579 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/ProcessVisitorDefault.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/ProcessVisitorDefault.java
@@ -1,55 +1,7 @@
package fr.inria.corese.core.next.query.impl.engine.event;
-import fr.inria.corese.core.next.query.impl.engine.eval.Eval;
-import fr.inria.corese.core.next.query.impl.engine.model.Node;
-import fr.inria.corese.core.next.query.impl.engine.pattern.Exp;
-import fr.inria.corese.core.next.query.impl.engine.solution.Mappings;
-
/**
- * Default process visitor that forwards reporting events to the query binding context
- * when query report is enabled.
+ * Default process visitor providing no-op implementations of execution lifecycle events.
*/
public final class ProcessVisitorDefault implements ProcessVisitor {
-
- void visit(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) {
- if (eval.getQuery().isReportEnabled()) {
- eval.getBind().visit(e, g, m1, m2);
- }
- }
-
- @Override
- public void graph(Eval eval, Node g, Exp e, Mappings m1) {
- visit(eval, g, e, m1, null);
- }
-
- @Override
- public void query(Eval eval, Node g, Exp e, Mappings m1) {
- visit(eval, g, e, m1, null);
- }
-
- @Override
- public void service(Eval eval, Node g, Exp e, Mappings m1) {
- visit(eval, g, e, m1, null);
- }
-
- @Override
- public void optional(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) {
- visit(eval, g, e, m1, m2);
- }
-
- @Override
- public void minus(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) {
- visit(eval, g, e, m1, m2);
- }
-
- @Override
- public void union(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) {
- visit(eval, g, e, m1, m2);
- }
-
- @Override
- public void join(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) {
- visit(eval, g, e, m1, m2);
- }
-
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/ResultListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/ResultListener.java
deleted file mode 100644
index 95b7956bf..000000000
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/ResultListener.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package fr.inria.corese.core.next.query.impl.engine.event;
-
-import fr.inria.corese.core.next.query.impl.engine.model.Edge;
-import fr.inria.corese.core.next.query.impl.engine.model.Expr;
-import fr.inria.corese.core.next.query.impl.engine.spi.Environment;
-import fr.inria.corese.core.next.query.impl.engine.pattern.Exp;
-import fr.inria.corese.core.next.query.impl.engine.path.Path;
-
-/**
- * Result Listener to process KGRAM result on the fly
- *
- * @author Olivier Corby, Edelweiss, INRIA 2011
- *
- */
-public interface ResultListener {
-
- /**
- * For each solution, kgram call process(env)
- * If return true: Mapping created as usual
- * If return false: Mapping not created
- */
- boolean process(Environment env);
-
-
- /**
- * For each path, kgram call process(path)
- * If return true: Mapping created as usual
- * If return false: Mapping not created
- */
- boolean process(Path path);
-
-
- boolean listen(Exp exp, Edge query, Edge target);
-
- Exp listen(Exp exp, int n);
-
- void listen(Expr exp);
-}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/BindingContext.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/BindingContext.java
index b0077dcf1..690fd2bfd 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/BindingContext.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/BindingContext.java
@@ -108,17 +108,4 @@ default ProcessVisitor getVisitor() {
default void setVisitor(ProcessVisitor visitor) {
// By default, do nothing
}
-
- /**
- * Visit method for reporting.
- * Used by ProcessVisitorDefault.
- *
- * @param e the expression
- * @param g the graph node
- * @param m1 the mappings 1
- * @param m2 the mappings 2
- */
- default void visit(Object e, Object g, Object m1, Object m2) {
- // By default, do nothing
- }
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/Edge.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/Edge.java
index b1c4a2d21..8a2183b7f 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/Edge.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/Edge.java
@@ -75,14 +75,6 @@ default boolean isMatchArity() {
return false;
}
- // nested rdf star triple <>
- default boolean isNested() {
- return false;
- }
- default void setNested(boolean b) {
- // RDF-star nesting is an optional capability of concrete edge implementations.
- }
-
default DatatypeValue getGraphValue() {
Node node = getGraph();
if (node == null) {
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/ExpType.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/ExpType.java
index bb4145050..64156a5ee 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/ExpType.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/ExpType.java
@@ -1,9 +1,7 @@
package fr.inria.corese.core.next.query.impl.engine.model;
/**
- * Types of expression of KGRAM query language
- *
- * @author Olivier Corby, Edelweiss, INRIA 2010
+ * Expression types for SPARQL query algebra in KGRAM engine.
*/
public interface ExpType {
@@ -17,31 +15,21 @@ enum Type {
FILTER("FILTER"),
NODE("NODE"),
BGP("BGP"),
- WATCH("WATCH"),
- CONTINUE("CONTINUE"),
- BACKJUMP("BACKJUMP"),
QUERY("QUERY"),
- FORALL("FORALL"),
EXIST("EXIST"),
GRAPHNODE("GRAPHNODE"),
OPTIONAL("OPTIONAL"),
- SCAN("SCAN"),
- IF("IF"),
- XPATH("XPATH"),
ACCEPT("ACCEPT"),
BIND("BIND"),
- EVAL("EVAL"),
- SCOPE("SCOPE"),
TEST("TEST"),
- NEXT("NEXT"),
MINUS("MINUS"),
- POP("POP"),
SERVICE("SERVICE"),
- RESTORE("RESTORE"),
JOIN("JOIN"),
VALUES("VALUES"),
OPT_BIND("OPT_BIND");
+
private final String title;
+
Type(String title) {
this.title = title;
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/ExprType.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/ExprType.java
index 2a0148f67..7fb12685a 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/ExprType.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/ExprType.java
@@ -42,8 +42,6 @@ public interface ExprType {
int DATATYPE = 30;
int DEBUG = 33;
- int KGRAM = 36;
- int XPATH = 38;
int UNNEST = 41;
int EXIST = 42;
@@ -66,11 +64,6 @@ public interface ExprType {
int URI = 74;
int TZ = 75;
- int TRIPLE = 88;
- int SUBJECT = 89;
- int PREDICATE = 90;
- int OBJECT = 91;
-
int EQNE = 109;
int EQ = 110;
@@ -91,20 +84,5 @@ public interface ExprType {
int NODE = 209;
- int LOAD = 212;
-
- int ID = 223;
- int TEST = 224;
-
- int TURTLE = 227;
-
int AGGREGATE = 276;
-
- int QUERY = 302;
- int EXTENSION = 303;
-
- int LIST = 309;
- int ERROR = 315;
- int EVAL = 325;
- int FOR = 327;
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/Exp.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/Exp.java
index d92e28d47..f6b874ad3 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/Exp.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/Exp.java
@@ -295,8 +295,7 @@ StringBuilder toString(StringBuilder sb, int n) {
nl(sb, 0);
sb.append(getMappings().toString(true));
indent(sb, n);
- } else if (type() != Type.WATCH && type() != Type.CONTINUE && type() != Type.BACKJUMP) {
- // Process normal types (skip WATCH, CONTINUE, BACKJUMP because of loop)
+ } else {
appendChildren(sb, n);
}
@@ -680,7 +679,7 @@ boolean distinct(Node qNode) {
for (int i = 0; i < size(); i++) {
Exp exp = get(i);
switch (exp.type()) {
- case EDGE, XPATH, EVAL:
+ case EDGE:
if (exp.contains(qNode)) {
add(i + 1, Exp.create(Type.ACCEPT, qNode));
return true;
@@ -708,7 +707,7 @@ public boolean isSortable() {
boolean isSimple() {
return switch (type) {
- case EDGE, EVAL, OPT_BIND -> true;
+ case EDGE, OPT_BIND -> true;
default -> false;
};
}
@@ -826,7 +825,7 @@ void addBind(Node node, List lVar) {
*/
int nbNode() {
switch (type) {
- case EDGE, EVAL:
+ case EDGE:
if (edge.getEdgeVariable() == null) {
return edge.nbNode();
} else {
@@ -846,7 +845,7 @@ int nbNode() {
*/
Node getNode(int n) {
switch (type) {
- case EDGE, EVAL:
+ case EDGE:
if (n < edge.nbNode()) {
return edge.getNode(n);
} else {
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpPatternScope.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpPatternScope.java
index 80a57137d..c5b1be34d 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpPatternScope.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpPatternScope.java
@@ -31,11 +31,6 @@ public static void getNodes(Exp exp, ExpNodeCollector h) {
}
case NODE, ACCEPT -> h.add(exp.getNode());
case EDGE -> collectEdgeNodes(exp, h);
- case XPATH, EVAL -> {
- for (int i = 0; i < exp.nbNode(); i++) {
- h.add(exp.getNode(i));
- }
- }
case VALUES -> {
if (exp.getNodeList() != null) {
for (Node varNode : exp.getNodeList()) {
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/QueryExpIndexer.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/QueryExpIndexer.java
index 5fd8f2ed0..7fc4ad714 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/QueryExpIndexer.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/QueryExpIndexer.java
@@ -57,11 +57,6 @@ void collect(Exp exp, boolean exist) {
}
}
case EDGE -> collectEdgeNodes(exp.getEdge(), exist);
- case XPATH, EVAL -> {
- for (int i = 0; i < exp.nbNode(); i++) {
- layout.store(exp.getNode(i), exist, false);
- }
- }
case MINUS -> collectMinusNodes(exp, exist);
case QUERY -> {
for (Exp ee : exp.getQuery().getSelectFun()) {
@@ -123,7 +118,7 @@ void collectExist(Expr exp) {
*/
public int index(QueryPlan query, Exp exp, boolean isExist, int start) {
int min = switch (exp.type()) {
- case EDGE, XPATH, EVAL -> indexEdgeExp(exp);
+ case EDGE -> indexEdgeExp(exp);
case VALUES -> indexValuesExp(exp);
case NODE -> queryPlan.index(exp.getNode());
case BIND -> indexBindExp(query, exp, isExist);
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/QueryPlan.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/QueryPlan.java
index fefe7d2a6..e98314cc4 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/QueryPlan.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/QueryPlan.java
@@ -66,7 +66,6 @@ public final class QueryPlan extends Exp {
// Extended queries for additional group by
List queries;
private boolean isCompiled = false;
- private boolean reportEnabled;
private boolean isUseBind = true;
@@ -140,7 +139,6 @@ private QueryPlan(QueryPlan source, PlanCopy copies) {
this.table = copies.copy(source.table);
this.queries = copies.copy(source.queries);
this.isCompiled = source.isCompiled;
- this.reportEnabled = source.reportEnabled;
this.isUseBind = source.isUseBind;
this.isRelax = source.isRelax;
this.isDistribute = source.isDistribute;
@@ -1073,19 +1071,6 @@ public boolean isUseBind() {
return isUseBind;
}
-
-
- public boolean isReportEnabled() {
- return getGlobalQuery().reportEnabled;
- }
-
- public void setReportEnabled(boolean reportEnabled) {
- getGlobalQuery().reportEnabled = reportEnabled;
- }
-
-
-
-
public Mapping getMapping() {
return mapping;
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanner.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanner.java
index a77c1a238..e47a39585 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanner.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanner.java
@@ -97,7 +97,7 @@ private Exp compileExpression(Exp exp, VString varList, boolean option) {
Type type = exp.type();
switch (type) {
- case EDGE, XPATH, EVAL, NODE, GRAPHNODE:
+ case EDGE, NODE, GRAPHNODE:
break;
case FILTER, BIND:
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/Memory.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/Memory.java
index d89ea9ba0..b40bbdfd6 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/Memory.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/Memory.java
@@ -238,15 +238,8 @@ public void copyInto(QueryPlan sub, Memory mem, Exp exp) {
if (sub == null) {
// exists pattern
copyInto(mem, exp);
- } else if (eval.getMode() != Evaluator.Mode.SPARQL_MODE) {
+ } else {
// bind subquery select nodes
- // take only from this memory the nodes
- // that are select nodes of sub query
- // hence sub query memory share only select node bindings
- // with outer query memory
- // use case: ?x :p ?z (select ?z where (?x :q ?z))
- // ?x in sub query is not the same as ?x in outer query (it is not bound here)
- // only ?z is the same
for (Node subNode : sub.getSelect()) {
copyInto(subNode, mem);
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Evaluator.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Evaluator.java
index 0a4f1b5d1..639c6253b 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Evaluator.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Evaluator.java
@@ -1,7 +1,5 @@
package fr.inria.corese.core.next.query.impl.engine.spi;
-import fr.inria.corese.core.next.query.impl.engine.eval.Eval;
-
import java.time.OffsetDateTime;
/**
@@ -21,16 +19,6 @@ public interface Evaluator {
*/
OffsetDateTime getQueryEvaluationTime();
- Mode getMode();
-
- void setMode(Mode mode);
-
- default void setProducer(Producer p) {
- }
-
- default void setKGRAM(Eval o) {
- }
-
default void start(Environment env) {
}
@@ -39,9 +27,4 @@ default void finish(Environment env) {
default void init(Environment env) {
}
-
- enum Mode {
- KGRAM_MODE, SPARQL_MODE
- }
-
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/query/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/query/package-info.java
index 5c5b7a9ba..7cbc8e37b 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/query/package-info.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/query/package-info.java
@@ -6,7 +6,7 @@
*
Prepared query classes extend
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepositoryConnection.java b/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepositoryConnection.java
index 95441b9ff..2eef161aa 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepositoryConnection.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepositoryConnection.java
@@ -6,7 +6,10 @@
import fr.inria.corese.core.next.data.api.io.parser.RDFParser;
import fr.inria.corese.core.next.data.api.io.serializer.RDFSerializer;
import fr.inria.corese.core.next.data.api.exception.SerializationException;
-import fr.inria.corese.core.next.io.CoreseIO;
+import fr.inria.corese.core.next.data.api.io.parser.RDFParserFactory;
+import fr.inria.corese.core.next.data.api.io.serializer.RDFSerializerFactory;
+import fr.inria.corese.core.next.data.impl.io.parser.DefaultRDFParserFactory;
+import fr.inria.corese.core.next.data.impl.io.serializer.DefaultRDFSerializerFactory;
import fr.inria.corese.core.next.storage.StorageModels;
import fr.inria.corese.core.next.data.api.term.IRI;
import fr.inria.corese.core.next.data.api.term.Resource;
@@ -63,6 +66,8 @@
public final class CoreseRepositoryConnection implements RepositoryConnection {
private static final String PARAM_DESTINATION = "destination";
+ private static final RDFParserFactory RDF_PARSER_FACTORY = new DefaultRDFParserFactory();
+ private static final RDFSerializerFactory RDF_SERIALIZER_FACTORY = new DefaultRDFSerializerFactory();
private final Repository repository;
private final StorageManager storage;
@@ -128,7 +133,7 @@ private void importData(RDFFormat format, Resource[] contexts, Consumer source = format.supportsNamedGraphs()
? statements : requireDefaultGraph(statements, format);
- action.accept(CoreseIO.serializer(source, format));
+ action.accept(RDF_SERIALIZER_FACTORY.createSerializer(format, source));
}
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlAstBuilder.java
index 4fb05e51f..fb28d910e 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlAstBuilder.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlAstBuilder.java
@@ -28,14 +28,14 @@ public abstract class SparqlAstBuilder {
protected final SparqlParserOptions options;
/**
- * Effective base URI after prologue (parser options, then possibly {@code BASE}).
+ * Prologue manager handling BASE URI and PREFIX declarations.
*/
- protected String baseUri;
+ protected final SparqlPrologueBuilder prologue;
/**
- * Prefix declarations in source order (including redeclarations).
+ * Expander lowering property list paths, blank node property lists and collections.
*/
- protected final List prefixDeclarations = new ArrayList<>();
+ protected final TriplePathExpander pathExpander;
/**
* Dataset clause (FROM/FROM NAMED)
@@ -188,13 +188,61 @@ public void enterGraph(TermAst name) {
@SuppressWarnings("this-escape") // Callbacks are stored locally and are not invoked or published during construction.
protected SparqlAstBuilder(SparqlParserOptions options) {
this.options = options;
- this.baseUri = options.getBaseIRI();
this.terms = new SparqlTermBuilder(this::termFromBlankNode);
this.paths = new SparqlPathBuilder(this.terms);
this.expressions = new SparqlExpressionBuilder(this.terms, this::popCapturedExistsPattern);
+ this.prologue = createPrologueBuilder(options.getBaseIRI());
+ this.pathExpander = new TriplePathExpander(this, this.terms, this.paths);
}
- private boolean baseDeclarationSeen = false;
+ protected SparqlPrologueBuilder createPrologueBuilder(String initialBaseUri) {
+ return new SparqlPrologueBuilder(initialBaseUri);
+ }
+
+ /**
+ * Term builder helper.
+ *
+ * @return active term builder
+ */
+ public SparqlTermBuilder terms() {
+ return this.terms;
+ }
+
+ /**
+ * Property-path builder helper.
+ *
+ * @return active path builder
+ */
+ public SparqlPathBuilder paths() {
+ return this.paths;
+ }
+
+ /**
+ * Expression and constraint builder helper.
+ *
+ * @return active expression builder
+ */
+ public SparqlExpressionBuilder expressions() {
+ return this.expressions;
+ }
+
+ /**
+ * Prologue builder helper.
+ *
+ * @return active prologue builder
+ */
+ public SparqlPrologueBuilder prologue() {
+ return this.prologue;
+ }
+
+ /**
+ * Triple path expander helper.
+ *
+ * @return active path expander
+ */
+ TriplePathExpander pathExpander() {
+ return this.pathExpander;
+ }
public void reserveBlankNodeLabels(Collection labels) {
blankNodeLabels.addAll(Objects.requireNonNull(labels, "labels"));
@@ -203,44 +251,19 @@ public void reserveBlankNodeLabels(Collection labels) {
// --- Construction entry points (called by listener) ---
public void setBaseUri(String uri) {
- if (this.baseDeclarationSeen) {
- throw new QuerySyntaxException("Base URI already set, multiple BASE declarations are forbidden.");
- }
- this.baseDeclarationSeen = true;
- this.baseUri = uri != null ? RdfText.stripAngleBrackets(uri) : null;
+ this.prologue.setBaseUri(uri);
}
public void addPrefix(String prefix, String uri) {
- String p = normalizePrefix(prefix);
- String ns = uri != null ? RdfText.stripAngleBrackets(uri) : "";
- if (this.baseUri != null && !IRIUtils.isAbsoluteIRI(ns)) {
- ns = IRIUtils.resolveIRIAgainstBase(this.baseUri, ns);
- }
- PrefixDeclarationAst declarationAst = new PrefixDeclarationAst(p, new IriAst(ns));
- if(this.prefixDeclarations.stream().anyMatch(declaration ->
- Objects.equals(declaration.prefix(), declarationAst.prefix())
- )) {
- throw new QuerySyntaxException("Prefix " + prefix + " has already been declared");
- }
- this.prefixDeclarations.add(declarationAst);
- }
-
- private static String normalizePrefix(String prefix) {
- if (prefix == null) {
- return "";
- }
- if (prefix.endsWith(":")) {
- return prefix.substring(0, prefix.length() - 1);
- }
- return prefix;
+ this.prologue.addPrefix(prefix, uri);
}
public String getBaseUri() {
- return this.baseUri;
+ return this.prologue.getBaseUri();
}
public List getPrefixDeclaration() {
- return this.prefixDeclarations;
+ return this.prologue.getPrefixDeclaration();
}
// --- Pattern creation ---
@@ -606,90 +629,22 @@ public GroupGraphPatternAst popCapturedExistsPattern() {
}
- // --- Creation Term helpers ---
-
- /**
- * Variable token text can be "?s" or "$s" depending on grammar.
- */
- public TermAst variable(String tokenText) {
- return terms.variable(tokenText);
- }
-
- /**
- * IRI term as raw text:
- *
- *
{@code }
- *
{@code foaf:Person}
- *
{@code a}
- *
- */
- public TermAst iri(String raw) {
- return terms.iri(raw);
- }
-
- /**
- * Literal term.
- * - lexical should be the literal lexical form (often including quotes at this stage)
- * - lang without '@' (e.g., "fr"), or null
- * - datatype as IRI/QName text (e.g., "xsd:integer"), or null
- */
- public TermAst literal(String lexical, String lang, String datatype) {
- return terms.literal(lexical, lang, datatype);
- }
-
-
-
- // ---- term helpers ----
-
- public TermAst termFromVerb(SparqlParser.VerbContext ctx) {
- return terms.termFromVerb(ctx);
- }
-
- public TermAst termFromVarOrTerm(SparqlParser.VarOrTermContext ctx) {
- return terms.termFromVarOrTerm(ctx);
- }
-
- public TermAst termFromVarOrIriRef(SparqlParser.VarOrIriContext ctx) {
- return terms.termFromVarOrIriRef(ctx);
- }
-
public List termListFromObjectList(SparqlParser.ObjectListContext ctx) {
return terms.termListFromObjectList(ctx, this::termFromObject);
}
- public TermAst termFromObject(SparqlParser.Object_Context ctx) {
- // object_ : graphNode
+ private TermAst termFromObject(SparqlParser.Object_Context ctx) {
return termFromGraphNode(ctx.graphNode());
}
- public TermAst termFromGraphNode(SparqlParser.GraphNodeContext ctx) {
- if (ctx.varOrTerm() != null) return termFromVarOrTerm(ctx.varOrTerm());
+ private TermAst termFromGraphNode(SparqlParser.GraphNodeContext ctx) {
+ if (ctx.varOrTerm() != null) {
+ return terms.termFromVarOrTerm(ctx.varOrTerm());
+ }
if (ctx.triplesNode() != null) {
- // MVP: pas encore supporté ( [ ... ] ou ( ... ) )
- return this.iri(ctx.triplesNode().getText());
+ return terms.iri(ctx.triplesNode().getText());
}
-
- return this.iri(ctx.getText());
- }
-
- public TermAst termFromRdfLiteral(SparqlParser.RdfLiteralContext ctx) {
- return terms.termFromRdfLiteral(ctx);
- }
-
- public TermAst termFromVar(SparqlParser.Var_Context ctx) {
- return terms.termFromVar(ctx);
- }
-
- public TermAst termFromBooleanLiteral(SparqlParser.BooleanLiteralContext ctx) {
- return terms.termFromBooleanLiteral(ctx);
- }
-
- public TermAst termFromIriRef(SparqlParser.IriRefContext ctx) {
- return terms.termFromIriRef(ctx);
- }
-
- public TermAst termFromNumericLiteral(SparqlParser.NumericLiteralContext ctx) {
- return terms.termFromNumericLiteral(ctx);
+ return terms.iri(ctx.getText());
}
public TermAst termFromBlankNode(SparqlParser.BlankNodeContext ctx) {
@@ -710,35 +665,7 @@ public TermAst termFromBlankNode(SparqlParser.BlankNodeContext ctx) {
"Blank node label '" + label + "' is used in more than one basic graph pattern");
}
}
- return this.iri(label);
- }
-
- /**
- * 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) {
- return expressions.termFromGroupCondition(ctx);
- }
-
- public TermAst termFromConstraint(SparqlParser.ConstraintContext ctx) {
- return expressions.termFromConstraint(ctx);
- }
-
- public TermAst termFromExpression(SparqlParser.ExpressionContext ctx) {
- return expressions.termFromExpression(ctx);
- }
-
- public TermAst termFromBrackettedExpression(SparqlParser.BrackettedExpressionContext ctx) {
- return expressions.termFromBrackettedExpression(ctx);
- }
-
- /**
- * Predicate as a SPARQL 1.1 property path.
- */
- public PathAst pathFromVerbPath(SparqlParser.VerbPathContext ctx) {
- return paths.pathFromVerbPath(ctx);
+ return terms.iri(label);
}
/**
@@ -749,7 +676,7 @@ public TermAst newAnonymousBlankNode() {
do {
label = GENERATED_BLANK_NODE_PREFIX + anonymousBlankNodeCounter++;
} while (!blankNodeLabels.add(label));
- return iri(label);
+ return terms.iri(label);
}
/**
@@ -758,99 +685,14 @@ public TermAst newAnonymousBlankNode() {
public void addTriplesFromPropertyListPath(
TermAst subject,
SparqlParser.PropertyListPathNotEmptyContext propertyList) {
- var verbPaths = propertyList.verbPath();
- var verbSimples = propertyList.verbSimple();
- var objectLists = propertyList.objectListPath();
-
- int verbPathIdx = 0;
- int verbSimpleIdx = 0;
-
- for (SparqlParser.ObjectListPathContext objectList : objectLists) {
- boolean useVerbPath = verbPathIdx < verbPaths.size()
- && (verbSimpleIdx >= verbSimples.size()
- || verbPaths.get(verbPathIdx).getStart().getTokenIndex()
- < verbSimples.get(verbSimpleIdx).getStart().getTokenIndex());
-
- PathAst predicate = useVerbPath
- ? pathFromVerbPath(verbPaths.get(verbPathIdx++))
- : new PredicatePathAst(terms.termFromVerbSimple(verbSimples.get(verbSimpleIdx++)));
-
- for (TermAst object : termListFromObjectListPath(objectList)) {
- addTriple(subject, predicate, object);
- }
- }
+ pathExpander.addTriplesFromPropertyListPath(subject, propertyList);
}
/**
* Builds triple patterns for a {@code triplesNodePath} subject and returns its head term.
*/
public TermAst subjectFromTriplesNodePath(SparqlParser.TriplesNodePathContext ctx) {
- if (ctx.blankNodePropertyListPath() != null) {
- return subjectFromBlankNodePropertyListPath(ctx.blankNodePropertyListPath());
- }
- if (ctx.collectionPath() != null) {
- return subjectFromCollectionPath(ctx.collectionPath());
- }
- throw new QueryEvaluationException("Unexpected triples node path: " + ctx.getText());
- }
-
- private TermAst subjectFromBlankNodePropertyListPath(
- SparqlParser.BlankNodePropertyListPathContext ctx) {
- TermAst blankNode = newAnonymousBlankNode();
- if (ctx.propertyListPathNotEmpty() != null) {
- addTriplesFromPropertyListPath(blankNode, ctx.propertyListPathNotEmpty());
- }
- return blankNode;
- }
-
- private TermAst subjectFromCollectionPath(SparqlParser.CollectionPathContext ctx) {
- List nodes = ctx.graphNodePath();
- if (nodes.isEmpty()) {
- throw new QueryEvaluationException("Empty RDF collection in triple pattern");
- }
-
- TermAst head = newAnonymousBlankNode();
- TermAst current = head;
- for (int i = 0; i < nodes.size(); i++) {
- addTriple(current, rdfTerm(RDF.first), termFromGraphNodePath(nodes.get(i)));
- if (i == nodes.size() - 1) {
- addTriple(current, rdfTerm(RDF.rest), rdfTerm(RDF.nil));
- } else {
- TermAst next = newAnonymousBlankNode();
- addTriple(current, rdfTerm(RDF.rest), next);
- current = next;
- }
- }
- return head;
- }
-
- /**
- * Graph node inside a property-path triple (variable/term, blank node property list, or collection).
- */
- public TermAst termFromGraphNodePath(SparqlParser.GraphNodePathContext ctx) {
- if (ctx.varOrTerm() != null) {
- return termFromVarOrTerm(ctx.varOrTerm());
- }
- if (ctx.triplesNodePath() != null) {
- return subjectFromTriplesNodePath(ctx.triplesNodePath());
- }
- throw new QueryEvaluationException("Unexpected graph node path: " + ctx.getText());
- }
-
- private TermAst rdfTerm(RDF term) {
- return new IriAst("<" + term.getIRI().stringValue() + ">");
- }
-
- /**
- * List of objects inside a property path triple.
- */
- @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline.
- public List termListFromObjectListPath(SparqlParser.ObjectListPathContext ctx) {
- List out = new ArrayList<>();
- for (var objPath : ctx.objectPath()) {
- out.add(termFromGraphNodePath(objPath.graphNodePath()));
- }
- return out;
+ return pathExpander.subjectFromTriplesNodePath(ctx);
}
public GraphRefAst graphRefFromGraphOrDefault(SparqlParser.GraphOrDefaultContext ctx) {
@@ -858,7 +700,7 @@ public GraphRefAst graphRefFromGraphOrDefault(SparqlParser.GraphOrDefaultContext
return GraphRefAsts.defaultGraph();
}
if(ctx.iriRef() != null) {
- IriAst graphIri = (IriAst) termFromIriRef(ctx.iriRef());
+ IriAst graphIri = (IriAst) terms.termFromIriRef(ctx.iriRef());
return GraphRefAsts.graph(graphIri);
}
throw new QueryEvaluationException("Unexpected value for Graph reference or default " + ctx.getText());
@@ -866,7 +708,7 @@ public GraphRefAst graphRefFromGraphOrDefault(SparqlParser.GraphOrDefaultContext
public GraphRefAst graphRefFromGraphRef(SparqlParser.GraphRefContext ctx) {
if(ctx.iriRef() != null) {
- IriAst graphIri = (IriAst) termFromIriRef(ctx.iriRef());
+ IriAst graphIri = (IriAst) terms.termFromIriRef(ctx.iriRef());
return GraphRefAsts.graph(graphIri);
}
throw new QueryEvaluationException("Unexpected value for Graph reference " + ctx.getText());
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlPrologueBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlPrologueBuilder.java
new file mode 100644
index 000000000..71347ccc6
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlPrologueBuilder.java
@@ -0,0 +1,82 @@
+package fr.inria.corese.core.next.query.impl.sparql.parser;
+
+import fr.inria.corese.core.next.common.text.RdfText;
+import fr.inria.corese.core.next.data.spi.term.IRIUtils;
+import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException;
+import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.PrefixDeclarationAst;
+import fr.inria.corese.core.next.query.impl.sparql.ast.QueryPrologueAst;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Manages BASE URI and PREFIX declarations during SPARQL query and update parsing.
+ */
+public class SparqlPrologueBuilder {
+
+ protected String baseUri;
+ protected boolean baseDeclarationSeen;
+ protected final List prefixDeclarations = new ArrayList<>();
+
+ public SparqlPrologueBuilder(final String initialBaseUri) {
+ this.baseUri = initialBaseUri;
+ }
+
+ public void setBaseUri(final String uri) {
+ if (this.baseDeclarationSeen) {
+ throw new QuerySyntaxException("Base URI already set, multiple BASE declarations are forbidden.");
+ }
+ this.baseDeclarationSeen = true;
+ this.baseUri = uri != null ? RdfText.stripAngleBrackets(uri) : null;
+ }
+
+ public void addPrefix(final String prefix, final String uri) {
+ final String p = normalizePrefix(prefix);
+ String ns = uri != null ? RdfText.stripAngleBrackets(uri) : "";
+ if (this.baseUri != null && !IRIUtils.isAbsoluteIRI(ns)) {
+ ns = IRIUtils.resolveIRIAgainstBase(this.baseUri, ns);
+ }
+ final PrefixDeclarationAst declarationAst = new PrefixDeclarationAst(p, new IriAst(ns));
+ if (this.prefixDeclarations.stream().anyMatch(declaration ->
+ Objects.equals(declaration.prefix(), declarationAst.prefix())
+ )) {
+ throw new QuerySyntaxException("Prefix " + prefix + " has already been declared");
+ }
+ this.prefixDeclarations.add(declarationAst);
+ }
+
+ public void replacePrefix(final String prefix, final String uri) {
+ final String name = RdfText.stripTrailingColon(prefix);
+ this.prefixDeclarations.removeIf(declaration -> declaration.prefix().equals(name));
+ addPrefix(prefix, uri);
+ }
+
+ public void updateBaseUri(final String uri) {
+ final String iri = RdfText.stripAngleBrackets(uri);
+ this.baseUri = IRIUtils.isAbsoluteIRI(iri) ? iri : IRIUtils.resolveIRIAgainstBase(this.baseUri, iri);
+ }
+
+ public static String normalizePrefix(final String prefix) {
+ if (prefix == null) {
+ return "";
+ }
+ if (prefix.endsWith(":")) {
+ return prefix.substring(0, prefix.length() - 1);
+ }
+ return prefix;
+ }
+
+ public String getBaseUri() {
+ return this.baseUri;
+ }
+
+ public List getPrefixDeclaration() {
+ return this.prefixDeclarations;
+ }
+
+ public QueryPrologueAst buildPrologue() {
+ return new QueryPrologueAst(List.copyOf(this.prefixDeclarations), new IriAst(this.baseUri));
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlQuadTemplateBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlQuadTemplateBuilder.java
index 53269c457..d331f07dd 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlQuadTemplateBuilder.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlQuadTemplateBuilder.java
@@ -15,15 +15,15 @@
* Lowers ANTLR Quad contexts into AST quad templates, including nested blank-node property lists and RDF lists.
*/
final class SparqlQuadTemplateBuilder {
- private final SparqlAstBuilder terms;
+ private final SparqlAstBuilder builder;
/**
* Constructs a quad template builder using term construction helpers from the AST builder.
*
- * @param terms the SPARQL AST builder providing term lowering
+ * @param builder the SPARQL AST builder providing term lowering
*/
- SparqlQuadTemplateBuilder(SparqlAstBuilder terms) {
- this.terms = terms;
+ SparqlQuadTemplateBuilder(SparqlAstBuilder builder) {
+ this.builder = builder;
}
/**
@@ -40,7 +40,7 @@ QuadsAst quads(SparqlParser.QuadsContext context) {
defaults.addAll(triples(template));
}
for (var graph : context.quadsNotTriples()) {
- named.add(new NamedGraphQuadsAst(terms.termFromVarOrIriRef(graph.varOrIri()),
+ named.add(new NamedGraphQuadsAst(builder.terms().termFromVarOrIriRef(graph.varOrIri()),
triples(graph.triplesTemplate())));
}
return new QuadsAst(defaults, named);
@@ -51,7 +51,7 @@ private List triples(SparqlParser.TriplesTemplateContext conte
for (var current = context; current != null; current = current.triplesTemplate()) {
var triple = current.triplesSameSubject();
if (triple.varOrTerm() != null) {
- properties(terms.termFromVarOrTerm(triple.varOrTerm()), triple.propertyListNotEmpty(), result);
+ properties(builder.terms().termFromVarOrTerm(triple.varOrTerm()), triple.propertyListNotEmpty(), result);
} else {
TermAst subject = nested(triple.triplesNode(), result);
properties(subject, triple.propertyList().propertyListNotEmpty(), result);
@@ -66,7 +66,7 @@ private void properties(TermAst subject, SparqlParser.PropertyListNotEmptyContex
return;
}
for (int index = 0; index < properties.verb().size(); index++) {
- TermAst predicate = terms.termFromVerb(properties.verb(index));
+ TermAst predicate = builder.terms().termFromVerb(properties.verb(index));
for (var object : properties.objectList(index).object_()) {
result.add(new TriplePatternAst(subject, predicate, node(object.graphNode(), result)));
}
@@ -75,11 +75,11 @@ private void properties(TermAst subject, SparqlParser.PropertyListNotEmptyContex
private TermAst node(SparqlParser.GraphNodeContext context, List result) {
return context.varOrTerm() == null ? nested(context.triplesNode(), result)
- : terms.termFromVarOrTerm(context.varOrTerm());
+ : builder.terms().termFromVarOrTerm(context.varOrTerm());
}
private TermAst nested(SparqlParser.TriplesNodeContext context, List result) {
- TermAst head = terms.newAnonymousBlankNode();
+ TermAst head = builder.newAnonymousBlankNode();
if (context.blankNodePropertyList() != null) {
properties(head, context.blankNodePropertyList().propertyListNotEmpty(), result);
} else {
@@ -92,7 +92,7 @@ private void collection(TermAst head, List nodes,
TermAst current = head;
for (int index = 0; index < nodes.size(); index++) {
result.add(new TriplePatternAst(current, rdf(RDF.first), node(nodes.get(index), result)));
- TermAst next = index == nodes.size() - 1 ? rdf(RDF.nil) : terms.newAnonymousBlankNode();
+ TermAst next = index == nodes.size() - 1 ? rdf(RDF.nil) : builder.newAnonymousBlankNode();
result.add(new TriplePatternAst(current, rdf(RDF.rest), next));
current = next;
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlUpdateAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlUpdateAstBuilder.java
index a5d119294..ba3dfc08a 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlUpdateAstBuilder.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlUpdateAstBuilder.java
@@ -50,15 +50,12 @@ public class SparqlUpdateAstBuilder extends SparqlQueryAstBuilder{
@Override
public void addPrefix(String prefix, String uri) {
- String name = RdfText.stripTrailingColon(prefix);
- prefixDeclarations.removeIf(declaration -> declaration.prefix().equals(name));
- super.addPrefix(prefix, uri);
+ this.prologue.replacePrefix(prefix, uri);
}
@Override
public void setBaseUri(String uri) {
- String iri = RdfText.stripAngleBrackets(uri);
- baseUri = IRIUtils.isAbsoluteIRI(iri) ? iri : IRIUtils.resolveIRIAgainstBase(baseUri, iri);
+ this.prologue.updateBaseUri(uri);
}
@Override
@@ -72,7 +69,7 @@ public LoadRequestAst loadToAst(SparqlParser.LoadContext ctx) {
GraphRefAst sourceGraphAst = null;
GraphRefAst targetGraphAst = null;
if(ctx.iriRef() != null) {
- sourceGraphAst = new GraphRefAst((IriAst) this.termFromIriRef(ctx.iriRef()));
+ sourceGraphAst = new GraphRefAst((IriAst) this.terms().termFromIriRef(ctx.iriRef()));
}
if(ctx.graphRef() != null) {
targetGraphAst = this.graphRefFromGraphRef(ctx.graphRef());
@@ -173,14 +170,14 @@ public ModifyRequestAst modifyToAst(SparqlParser.ModifyContext context) {
Set defaults = new LinkedHashSet<>();
Set named = new LinkedHashSet<>();
for (SparqlParser.UsingClauseContext using : context.usingClause()) {
- IriAst iri = (IriAst) termFromIriRef(using.iriRef());
+ IriAst iri = (IriAst) terms().termFromIriRef(using.iriRef());
if (using.NAMED() == null) {
defaults.add(iri);
} else {
named.add(iri);
}
}
- IriAst with = context.iriRef() == null ? null : (IriAst) termFromIriRef(context.iriRef());
+ IriAst with = context.iriRef() == null ? null : (IriAst) terms().termFromIriRef(context.iriRef());
return new ModifyRequestAst(with, delete, insert, new DatasetClauseAst(defaults, named),
(SelectQueryAst) super.getResult());
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/TriplePathExpander.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/TriplePathExpander.java
new file mode 100644
index 000000000..511af92bc
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/TriplePathExpander.java
@@ -0,0 +1,141 @@
+package fr.inria.corese.core.next.query.impl.sparql.parser;
+
+import fr.inria.corese.core.next.data.api.vocabulary.RDF;
+import fr.inria.corese.core.next.generated.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.TermAst;
+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.ArrayList;
+import java.util.List;
+
+/**
+ * Expands SPARQL property list paths, blank node property lists, and RDF collections into triple patterns.
+ */
+final class TriplePathExpander {
+
+ private final SparqlAstBuilder builder;
+ private final SparqlTermBuilder terms;
+ private final SparqlPathBuilder paths;
+
+ TriplePathExpander(final SparqlAstBuilder builder, final SparqlTermBuilder terms, final SparqlPathBuilder paths) {
+ this.builder = builder;
+ this.terms = terms;
+ this.paths = paths;
+ }
+
+ /**
+ * Expands a {@code propertyListPathNotEmpty} into triple patterns for the given subject.
+ *
+ * @param subject subject term for the expanded triple patterns
+ * @param propertyList parse context containing verb paths, simple verbs, and object list paths
+ */
+ void addTriplesFromPropertyListPath(
+ final TermAst subject,
+ final SparqlParser.PropertyListPathNotEmptyContext propertyList) {
+ final var verbPaths = propertyList.verbPath();
+ final var verbSimples = propertyList.verbSimple();
+ final var objectLists = propertyList.objectListPath();
+
+ int verbPathIdx = 0;
+ int verbSimpleIdx = 0;
+
+ for (final SparqlParser.ObjectListPathContext objectList : objectLists) {
+ final boolean useVerbPath = verbPathIdx < verbPaths.size()
+ && (verbSimpleIdx >= verbSimples.size()
+ || verbPaths.get(verbPathIdx).getStart().getTokenIndex()
+ < verbSimples.get(verbSimpleIdx).getStart().getTokenIndex());
+
+ final PathAst predicate = useVerbPath
+ ? paths.pathFromVerbPath(verbPaths.get(verbPathIdx++))
+ : new PredicatePathAst(terms.termFromVerbSimple(verbSimples.get(verbSimpleIdx++)));
+
+ for (final TermAst object : termListFromObjectListPath(objectList)) {
+ builder.addTriple(subject, predicate, object);
+ }
+ }
+ }
+
+ /**
+ * Builds triple patterns for a {@code triplesNodePath} subject and returns its head term.
+ *
+ * @param ctx parse context for triples node path
+ * @return head term of the expanded structure
+ */
+ TermAst subjectFromTriplesNodePath(final SparqlParser.TriplesNodePathContext ctx) {
+ if (ctx.blankNodePropertyListPath() != null) {
+ return subjectFromBlankNodePropertyListPath(ctx.blankNodePropertyListPath());
+ }
+ if (ctx.collectionPath() != null) {
+ return subjectFromCollectionPath(ctx.collectionPath());
+ }
+ throw new QueryEvaluationException("Unexpected triples node path: " + ctx.getText());
+ }
+
+ private TermAst subjectFromBlankNodePropertyListPath(
+ final SparqlParser.BlankNodePropertyListPathContext ctx) {
+ final TermAst blankNode = builder.newAnonymousBlankNode();
+ if (ctx.propertyListPathNotEmpty() != null) {
+ addTriplesFromPropertyListPath(blankNode, ctx.propertyListPathNotEmpty());
+ }
+ return blankNode;
+ }
+
+ private TermAst subjectFromCollectionPath(final SparqlParser.CollectionPathContext ctx) {
+ final List nodes = ctx.graphNodePath();
+ if (nodes.isEmpty()) {
+ throw new QueryEvaluationException("Empty RDF collection in triple pattern");
+ }
+
+ final TermAst head = builder.newAnonymousBlankNode();
+ TermAst current = head;
+ for (int i = 0; i < nodes.size(); i++) {
+ builder.addTriple(current, rdfTerm(RDF.first), termFromGraphNodePath(nodes.get(i)));
+ if (i == nodes.size() - 1) {
+ builder.addTriple(current, rdfTerm(RDF.rest), rdfTerm(RDF.nil));
+ } else {
+ final TermAst next = builder.newAnonymousBlankNode();
+ builder.addTriple(current, rdfTerm(RDF.rest), next);
+ current = next;
+ }
+ }
+ return head;
+ }
+
+ /**
+ * Graph node inside a property-path triple (variable/term, blank node property list, or collection).
+ *
+ * @param ctx parse context for graph node path
+ * @return lowered term
+ */
+ TermAst termFromGraphNodePath(final SparqlParser.GraphNodePathContext ctx) {
+ if (ctx.varOrTerm() != null) {
+ return terms.termFromVarOrTerm(ctx.varOrTerm());
+ }
+ if (ctx.triplesNodePath() != null) {
+ return subjectFromTriplesNodePath(ctx.triplesNodePath());
+ }
+ throw new QueryEvaluationException("Unexpected graph node path: " + ctx.getText());
+ }
+
+ private static TermAst rdfTerm(final RDF term) {
+ return new IriAst("<" + term.getIRI().stringValue() + ">");
+ }
+
+ /**
+ * List of objects inside a property path triple.
+ *
+ * @param ctx parse context for object list path
+ * @return lowered terms
+ */
+ @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline.
+ List termListFromObjectListPath(final SparqlParser.ObjectListPathContext ctx) {
+ final List out = new ArrayList<>();
+ for (final var objPath : ctx.objectPath()) {
+ out.add(termFromGraphNodePath(objPath.graphNodePath()));
+ }
+ return out;
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/BgpAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/BgpAstListener.java
index 682bbcaf4..fa23dac0c 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/BgpAstListener.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/BgpAstListener.java
@@ -31,7 +31,7 @@ public void enterWhereClause(SparqlParser.WhereClauseContext ctx) {
@Override
public void enterGraphGraphPattern(SparqlParser.GraphGraphPatternContext ctx) {
- builder().enterGraph(builder().termFromVarOrIriRef(ctx.varOrIri()));
+ builder().enterGraph(builder().terms().termFromVarOrIriRef(ctx.varOrIri()));
}
@Override
@@ -79,10 +79,10 @@ public void exitTriplesSameSubject(SparqlParser.TriplesSameSubjectContext ctx) {
return;
}
SparqlAstBuilder b = builder();
- TermAst subject = b.termFromVarOrTerm(ctx.varOrTerm());
+ TermAst subject = b.terms().termFromVarOrTerm(ctx.varOrTerm());
var propertyList = ctx.propertyListNotEmpty();
for (int verbIndex = 0; verbIndex < propertyList.verb().size(); verbIndex++) {
- TermAst predicate = b.termFromVerb(propertyList.verb(verbIndex));
+ TermAst predicate = b.terms().termFromVerb(propertyList.verb(verbIndex));
List objects = b.termListFromObjectList(propertyList.objectList(verbIndex));
for (TermAst object : objects) {
b.addTriple(subject, predicate, object);
@@ -98,7 +98,7 @@ public void exitTriplesSameSubjectPath(SparqlParser.TriplesSameSubjectPathContex
if (propertyList == null) {
return;
}
- b.addTriplesFromPropertyListPath(b.termFromVarOrTerm(ctx.varOrTerm()), propertyList);
+ b.addTriplesFromPropertyListPath(b.terms().termFromVarOrTerm(ctx.varOrTerm()), propertyList);
return;
}
if (ctx.triplesNodePath() == null) {
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/BindAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/BindAstListener.java
index 8ec440442..8ec84832e 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/BindAstListener.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/BindAstListener.java
@@ -17,8 +17,8 @@ public BindAstListener(SparqlAstBuilder builder) {
@Override
public void exitBind(SparqlParser.BindContext ctx) {
- TermAst expression = builder().termFromExpression(ctx.expression());
- VarAst variable = (VarAst) builder().variable(ctx.var_().getText());
+ TermAst expression = builder().expressions().termFromExpression(ctx.expression());
+ VarAst variable = (VarAst) builder().terms().variable(ctx.var_().getText());
builder().addBind(new BindAst(expression, variable));
}
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/ConstructQueryAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/ConstructQueryAstListener.java
index 5931e0879..ae863f914 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/ConstructQueryAstListener.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/ConstructQueryAstListener.java
@@ -67,7 +67,7 @@ public void exitTriplesSameSubject(SparqlParser.TriplesSameSubjectContext ctx) {
return;
}
if (ctx.varOrTerm() != null && ctx.propertyListNotEmpty() != null) {
- TermAst subject = queryBuilder().termFromVarOrTerm(ctx.varOrTerm());
+ TermAst subject = queryBuilder().terms().termFromVarOrTerm(ctx.varOrTerm());
addConstructProperties(subject, ctx.propertyListNotEmpty(), inConstructWhere);
} else if (ctx.triplesNode() != null) {
TermAst subject = expandTriplesNode(ctx.triplesNode(), inConstructWhere);
@@ -93,7 +93,7 @@ private TermAst expandTriplesNode(SparqlParser.TriplesNodeContext ctx, boolean i
if (ctx.collection() != null) {
return expandCollection(ctx.collection(), inConstructWhere);
}
- return queryBuilder().iri(ctx.getText());
+ return queryBuilder().terms().iri(ctx.getText());
}
/**
@@ -103,11 +103,11 @@ private TermAst expandTriplesNode(SparqlParser.TriplesNodeContext ctx, boolean i
private TermAst expandCollection(SparqlParser.CollectionContext ctx, boolean inConstructWhere) {
var nodes = ctx.graphNode();
if (nodes.isEmpty()) {
- return queryBuilder().iri("");
+ return queryBuilder().terms().iri("");
}
- TermAst first = queryBuilder().iri("");
- TermAst rest = queryBuilder().iri("");
- TermAst nil = queryBuilder().iri("");
+ TermAst first = queryBuilder().terms().iri("");
+ TermAst rest = queryBuilder().terms().iri("");
+ TermAst nil = queryBuilder().terms().iri("");
TermAst head = queryBuilder().newAnonymousBlankNode();
TermAst current = head;
for (int i = 0; i < nodes.size(); i++) {
@@ -131,7 +131,7 @@ private TermAst termFromConstructGraphNode(SparqlParser.GraphNodeContext ctx, bo
if (ctx.triplesNode() != null) {
return expandTriplesNode(ctx.triplesNode(), inConstructWhere);
}
- return queryBuilder().termFromVarOrTerm(ctx.varOrTerm());
+ return queryBuilder().terms().termFromVarOrTerm(ctx.varOrTerm());
}
private void addConstructProperties(
@@ -139,7 +139,7 @@ private void addConstructProperties(
SparqlParser.PropertyListNotEmptyContext propertyList,
boolean inConstructWhere) {
for (int verbIndex = 0; verbIndex < propertyList.verb().size(); verbIndex++) {
- TermAst predicate = queryBuilder().termFromVerb(propertyList.verb(verbIndex));
+ TermAst predicate = queryBuilder().terms().termFromVerb(propertyList.verb(verbIndex));
for (var objectContext : propertyList.objectList(verbIndex).object_()) {
TermAst object = termFromConstructGraphNode(objectContext.graphNode(), inConstructWhere);
queryBuilder().addConstructTriple(subject, predicate, object);
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/DatasetClauseAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/DatasetClauseAstListener.java
index f7ee7f44e..a4f4ab727 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/DatasetClauseAstListener.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/DatasetClauseAstListener.java
@@ -11,11 +11,11 @@ public DatasetClauseAstListener(SparqlAstBuilder builder) {
@Override
public void exitDefaultGraphClause(SparqlParser.DefaultGraphClauseContext ctx) {
- builder().addFromGraph((IriAst) builder().termFromIriRef(ctx.sourceSelector().iriRef()));
+ builder().addFromGraph((IriAst) builder().terms().termFromIriRef(ctx.sourceSelector().iriRef()));
}
@Override
public void exitNamedGraphClause(SparqlParser.NamedGraphClauseContext ctx) {
- builder().addFromNamedGraph((IriAst) builder().termFromIriRef(ctx.sourceSelector().iriRef()));
+ builder().addFromNamedGraph((IriAst) builder().terms().termFromIriRef(ctx.sourceSelector().iriRef()));
}
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/DescribeQueryAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/DescribeQueryAstListener.java
index 9fc8e6a41..6a93f3f96 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/DescribeQueryAstListener.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/DescribeQueryAstListener.java
@@ -60,9 +60,9 @@ public void exitDescribeQuery(SparqlParser.DescribeQueryContext ctx) {
for (SparqlParser.VarOrIriContext ref : ctx.varOrIri()) {
String txt = ref.getText();
if (txt.startsWith("?") || txt.startsWith("$")) {
- queryBuilder().addDescribeResource(builder().variable(txt));
+ queryBuilder().addDescribeResource(builder().terms().variable(txt));
} else {
- queryBuilder().addDescribeResource(builder().iri(txt));
+ queryBuilder().addDescribeResource(builder().terms().iri(txt));
}
}
queryBuilder().exitDescribeQuery();
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/FilterAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/FilterAstListener.java
index f29214f88..ccae6819d 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/FilterAstListener.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/FilterAstListener.java
@@ -19,7 +19,7 @@ public void exitFilter_(SparqlParser.Filter_Context ctx) {
FilterAst filter;
if(ctx.constraint() != null) {
- filter = new FilterAst(this.builder().termFromConstraint(ctx.constraint()));
+ filter = new FilterAst(this.builder().expressions().termFromConstraint(ctx.constraint()));
} else {
throw new QueryEvaluationException("Empty filter " + ctx.getText() + ", no createFunCall found");
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/HavingAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/HavingAstListener.java
index b253c4fd9..bec0e8aaa 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/HavingAstListener.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/HavingAstListener.java
@@ -24,7 +24,7 @@ public void exitHavingClause(SparqlParser.HavingClauseContext ctx) {
private TermAst havingConditionToTerm(SparqlParser.HavingConditionContext ctx) {
if (ctx.constraint() != null) {
- return builder().termFromConstraint(ctx.constraint());
+ return builder().expressions().termFromConstraint(ctx.constraint());
}
throw new QueryEvaluationException("Unsupported HAVING condition: " + ctx.getText());
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/SelectQueryAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/SelectQueryAstListener.java
index f4476a619..df4331473 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/SelectQueryAstListener.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/SelectQueryAstListener.java
@@ -99,7 +99,7 @@ private void extractProjection(SparqlParser.SelectClauseContext ctx) {
"Variable ?" + varName + " introduced by SELECT expression is already projected");
}
expressionBoundVars.add(varName);
- TermAst expressionAst = builder().termFromExpression(selectVar.expression());
+ TermAst expressionAst = builder().expressions().termFromExpression(selectVar.expression());
expressionTerms.put(varName, expressionAst);
expressionReferencedVariables.put(varName, variableScopeAnalyzer.collectReferencedVariables(expressionAst));
} else if (selectVar.var_() != null) {
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/ServiceAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/ServiceAstListener.java
index 668506eee..22e1b5bb0 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/ServiceAstListener.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/ServiceAstListener.java
@@ -42,7 +42,7 @@ public ServiceAstListener(SparqlAstBuilder builder) {
@Override
public void enterServiceGraphPattern(SparqlParser.ServiceGraphPatternContext ctx) {
boolean silent = ctx.SILENT() != null;
- TermAst endpoint = builder().termFromVarOrIriRef(ctx.varOrIri());
+ TermAst endpoint = builder().terms().termFromVarOrIriRef(ctx.varOrIri());
builder().enterService(endpoint, silent);
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/SolutionModifierAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/SolutionModifierAstListener.java
index 661cfe6d8..b5637391d 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/SolutionModifierAstListener.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/SolutionModifierAstListener.java
@@ -26,7 +26,7 @@ public void exitGroupClause(SparqlParser.GroupClauseContext ctx) {
Set expressionBoundVariables = new LinkedHashSet<>();
Map expressionTerms = new LinkedHashMap<>();
for (SparqlParser.GroupConditionContext gcc : ctx.groupCondition()) {
- TermAst expression = builder().termFromGroupCondition(gcc);
+ TermAst expression = builder().expressions().termFromGroupCondition(gcc);
terms.add(expression);
if (gcc.expression() != null && gcc.var_() != null) {
String aliasName = normalizeVariableName(gcc.var_().getText());
@@ -69,11 +69,11 @@ public void exitOrderCondition(SparqlParser.OrderConditionContext ctx) {
TermAst orderExpression;
if(ctx.brackettedExpression() != null) {
- orderExpression = this.builder().termFromBrackettedExpression(ctx.brackettedExpression());
+ orderExpression = this.builder().expressions().termFromBrackettedExpression(ctx.brackettedExpression());
} else if(ctx.constraint() != null) {
- orderExpression = builder().termFromConstraint(ctx.constraint());
+ orderExpression = builder().expressions().termFromConstraint(ctx.constraint());
} else if(ctx.var_() != null) {
- orderExpression = builder().termFromVar(ctx.var_());
+ orderExpression = builder().terms().termFromVar(ctx.var_());
} else {
throw new QueryEvaluationException("Unexpected expression in ORDER BY clause " + ctx.getText());
}
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/ValuesAstListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/ValuesAstListener.java
index afac5d47b..082a42196 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/ValuesAstListener.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/listener/ValuesAstListener.java
@@ -45,7 +45,7 @@ private ValuesAst processInlineDataOneVar(SparqlParser.InlineDataOneVarContext c
if (ctx.var_() == null) {
throw new QuerySyntaxException("Missing variable for solution mapping in VALUES clause");
}
- VarAst variable = (VarAst) builder().termFromVar(ctx.var_());
+ VarAst variable = (VarAst) builder().terms().termFromVar(ctx.var_());
List header = List.of(variable);
List rows = new ArrayList<>();
for (var value : ctx.dataBlockValue()) {
@@ -67,13 +67,13 @@ private Map termAstFromDataBlockValues(List variables,
VarAst variable = variables.get(varNum);
SparqlParser.DataBlockValueContext dataBlockValueContext = dataBlockValueList.get(varNum);
if(dataBlockValueContext.iriRef() != null) {
- valuesList.put(variable, this.builder().termFromIriRef(dataBlockValueContext.iriRef()));
+ valuesList.put(variable, this.builder().terms().termFromIriRef(dataBlockValueContext.iriRef()));
} else if(dataBlockValueContext.rdfLiteral() != null) {
- valuesList.put(variable, this.builder().termFromRdfLiteral(dataBlockValueContext.rdfLiteral()));
+ valuesList.put(variable, this.builder().terms().termFromRdfLiteral(dataBlockValueContext.rdfLiteral()));
} else if(dataBlockValueContext.numericLiteral() != null) {
- valuesList.put(variable, this.builder().termFromNumericLiteral(dataBlockValueContext.numericLiteral()));
+ valuesList.put(variable, this.builder().terms().termFromNumericLiteral(dataBlockValueContext.numericLiteral()));
} else if(dataBlockValueContext.booleanLiteral() != null) {
- valuesList.put(variable, this.builder().termFromBooleanLiteral(dataBlockValueContext.booleanLiteral()));
+ valuesList.put(variable, this.builder().terms().termFromBooleanLiteral(dataBlockValueContext.booleanLiteral()));
} else if(dataBlockValueContext.UNDEF() != null) {
valuesList.put(variable, null);
}
@@ -85,7 +85,7 @@ private Map termAstFromDataBlockValues(List variables,
private ValuesAst processInlineDataFull(SparqlParser.InlineDataFullContext ctx) {
List varList = new ArrayList<>();
for (var variable : ctx.var_()) {
- varList.add((VarAst) builder().termFromVar(variable));
+ varList.add((VarAst) builder().terms().termFromVar(variable));
}
List valuesList = new ArrayList<>();
for (var row : ctx.dataBlockValues()) {
diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/update/UpdateLoader.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/update/UpdateLoader.java
index 1988057ae..82c964442 100644
--- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/update/UpdateLoader.java
+++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/update/UpdateLoader.java
@@ -8,7 +8,7 @@
import fr.inria.corese.core.next.data.api.term.BNode;
import fr.inria.corese.core.next.data.api.term.Resource;
import fr.inria.corese.core.next.data.api.term.Value;
-import fr.inria.corese.core.next.io.CoreseIO;
+import fr.inria.corese.core.next.data.impl.io.parser.DefaultRDFParserFactory;
import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException;
import fr.inria.corese.core.next.storage.api.StorageManager;
@@ -61,7 +61,7 @@ private static Model read(String source) {
RDFFormat format = RDFFormat.byMimeType(connection.getContentType())
.or(() -> RDFFormat.byExtension(extension)).orElse(RDFFormat.TURTLE);
Model model = Models.create();
- CoreseIO.rdfParserFactory().createRDFParser(format, model, Values.factory()).parse(stream, source);
+ new DefaultRDFParserFactory().createRDFParser(format, model, Values.factory()).parse(stream, source);
return model;
}
} catch (IOException | IllegalArgumentException e) {
diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/GraphPartition.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/GraphPartition.java
new file mode 100644
index 000000000..a49d92c71
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/GraphPartition.java
@@ -0,0 +1,208 @@
+package fr.inria.corese.core.next.storage.impl.memory;
+
+import fr.inria.corese.core.next.data.api.model.Statement;
+import fr.inria.corese.core.next.data.api.term.Resource;
+import fr.inria.corese.core.next.data.api.term.Value;
+import fr.inria.corese.core.next.storage.api.model.StatementPattern;
+
+/**
+ * One graph and its three permutations (SPO, POS, OSP).
+ *
+ *
+ * References count State owners, not statements or rows.
+ *
+ */
+final class GraphPartition {
+
+ /**
+ * Number of live State owners, guarded by the database monitor.
+ */
+ int references = 1;
+
+ /**
+ * Stable graph registry position.
+ */
+ final long ordinal;
+
+ /**
+ * Canonical graph identifier; null denotes the default graph.
+ */
+ final Resource context;
+
+ /**
+ * Subject/predicate/object permutation of this graph.
+ */
+ final TripleIndex spo;
+
+ /**
+ * Predicate/object/subject permutation of this graph.
+ */
+ final TripleIndex pos;
+
+ /**
+ * Object/subject/predicate permutation of this graph.
+ */
+ final TripleIndex osp;
+
+ /**
+ * Creates a graph partition owning the supplied permutations or empty indexes.
+ *
+ * @param context canonical graph identifier; null denotes the default graph
+ * @param ordinal stable graph registry ordinal
+ */
+ GraphPartition(final Resource context, final long ordinal) {
+ this(context, ordinal, new TripleIndex(TripleIndex.Order.SPO), new TripleIndex(TripleIndex.Order.POS),
+ new TripleIndex(TripleIndex.Order.OSP));
+ }
+
+ /**
+ * Creates a graph partition owning the supplied permutations or empty indexes.
+ *
+ * @param context canonical graph identifier; null denotes the default graph
+ * @param ordinal stable graph registry ordinal
+ * @param spo SPO index owning immutable statements and canonical keys
+ * @param pos POS permutation of the same graph partition
+ * @param osp OSP permutation of the same graph partition
+ */
+ GraphPartition(final Resource context, final long ordinal, final TripleIndex spo, final TripleIndex pos,
+ final TripleIndex osp) {
+ this.context = context;
+ this.ordinal = ordinal;
+ this.spo = spo;
+ this.pos = pos;
+ this.osp = osp;
+ }
+
+ /**
+ * Constructs a complete partition and releases its SPO staging dictionary before publication.
+ *
+ * @param spo SPO index owning immutable statements and canonical keys
+ * @param context canonical graph identifier; null denotes the default graph
+ * @param ordinal stable graph registry ordinal
+ * @return a complete graph partition with no retained staging dictionary
+ */
+ static GraphPartition build(final TripleIndex spo, final Resource context, final long ordinal) {
+ final SecondaryIndexBuilder.Result secondary = SecondaryIndexBuilder.build(spo);
+ final GraphPartition partition = new GraphPartition(context, ordinal, spo, secondary.pos(), secondary.osp());
+ spo.finishStaging();
+ return partition;
+ }
+
+ /**
+ * Copies owned mutable containers while sharing immutable keys and statements.
+ *
+ * @return an independently owned copy of the mutable containers
+ */
+ GraphPartition copy() {
+ return new GraphPartition(context, ordinal, spo.copy(), pos.copy(), osp.copy());
+ }
+
+ /**
+ * Adds a statement or keyed value if it is absent.
+ *
+ * @param s captured statement to index
+ * @return true when an absent entry was added, false when it was already present
+ */
+ boolean add(Statement s) {
+ final TermKey subject = key(s.getSubject(), null, null, spo, pos, osp);
+ final TermKey predicate = key(s.getPredicate(), subject, null, pos, spo, osp);
+ final TermKey object = key(s.getObject(), subject, predicate, osp, spo, pos);
+ s = StatementCapture.sharedStatement(s, subject, predicate, object, context);
+ if (!spo.add(s, subject, predicate, object, osp)) {
+ return false;
+ }
+ pos.add(s, subject, predicate, object, spo);
+ osp.add(s, subject, predicate, object, pos);
+ return true;
+ }
+
+ /**
+ * Reuses a canonical RDF term key across roles, creating one only for a previously unseen term.
+ *
+ * @param value non-null RDF value or entry value
+ * @param subject canonical subject key, or null for a wildcard search
+ * @param predicate canonical predicate key; null denotes a wildcard for search operations
+ * @param primary preferred role-root index for canonical lookup
+ * @param secondary second role-root index for canonical lookup
+ * @param tertiary third role-root index for canonical lookup
+ * @return a reusable canonical key, or a newly captured key for an unseen RDF term
+ */
+ private TermKey key(final Value value, final TermKey subject, final TermKey predicate,
+ final TripleIndex primary, final TripleIndex secondary, final TripleIndex tertiary) {
+ final int hash = TermKey.structuralHash(value);
+ if (subject != null && subject.matchesLookup(value, hash)) {
+ return subject;
+ }
+ if (predicate != null && predicate.matchesLookup(value, hash)) {
+ return predicate;
+ }
+ // Role roots retain canonical keys; absent RDF terms require a new key.
+ TermKey canonical = primary.existingKey(value, hash);
+ if (canonical == null) {
+ canonical = secondary.existingKey(value, hash);
+ }
+ if (canonical == null) {
+ canonical = tertiary.existingKey(value, hash);
+ }
+ return canonical == null ? new TermKey(value, hash) : canonical;
+ }
+
+ /**
+ * Removes matching content while preserving the order of survivors.
+ *
+ * @param s captured statement to index
+ * @return true when an entry was removed, false when it was absent
+ */
+ boolean remove(final Statement s) {
+ final TermKey subject = new TermKey(s.getSubject());
+ final TermKey predicate = new TermKey(s.getPredicate());
+ final TermKey object = new TermKey(s.getObject());
+ if (!spo.remove(subject, predicate, object)) {
+ return false;
+ }
+ pos.remove(subject, predicate, object);
+ osp.remove(subject, predicate, object);
+ return true;
+ }
+
+ /**
+ * Removes selected index content with stable filtering and unchanged compaction rules.
+ *
+ * @param pattern captured search pattern
+ */
+ void removeMatches(final StatementPattern pattern) {
+ final TermKey subject = TermKey.nullable(pattern.getSubject());
+ final TermKey predicate = TermKey.nullable(pattern.getPredicate());
+ final TermKey object = TermKey.nullable(pattern.getObject());
+ spo.removeMatches(subject, predicate, object);
+ pos.removeMatches(subject, predicate, object);
+ osp.removeMatches(subject, predicate, object);
+ }
+
+ /**
+ * Selects the permutation appropriate for the bound prefix of a search pattern.
+ *
+ * @param p captured search pattern
+ * @return the SPO, POS or OSP permutation selected for this pattern
+ */
+ TripleIndex index(final StatementPattern p) {
+ if (p.getSubject() != null && (p.getPredicate() != null || p.getObject() == null)) {
+ return spo;
+ }
+ if (p.getPredicate() != null) {
+ return pos;
+ }
+ return p.getObject() != null ? osp : spo;
+ }
+
+ /**
+ * Reports whether the session-visible or retained graph content contains a match.
+ *
+ * @param statement statement whose RDF identities have been captured
+ * @return true when at least one matching statement exists
+ */
+ boolean contains(final Statement statement) {
+ return spo.count(
+ StatementPattern.of(statement.getSubject(), statement.getPredicate(), statement.getObject())) != 0;
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/IndexedStatementStore.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/IndexedStatementStore.java
index 8e8f31409..3e8372ad8 100644
--- a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/IndexedStatementStore.java
+++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/IndexedStatementStore.java
@@ -1,26 +1,9 @@
package fr.inria.corese.core.next.storage.impl.memory;
-import fr.inria.corese.core.next.data.Values;
-import fr.inria.corese.core.next.data.api.model.SimpleStatement;
import fr.inria.corese.core.next.data.api.model.Statement;
-import fr.inria.corese.core.next.data.api.term.BNode;
import fr.inria.corese.core.next.data.api.term.IRI;
-import fr.inria.corese.core.next.data.api.term.Literal;
import fr.inria.corese.core.next.data.api.term.Resource;
-import fr.inria.corese.core.next.data.api.term.SimpleBNode;
-import fr.inria.corese.core.next.data.api.term.SimpleDate;
-import fr.inria.corese.core.next.data.api.term.SimpleDateTime;
-import fr.inria.corese.core.next.data.api.term.SimpleDecimal;
-import fr.inria.corese.core.next.data.api.term.SimpleDouble;
-import fr.inria.corese.core.next.data.api.term.SimpleDuration;
-import fr.inria.corese.core.next.data.api.term.SimpleIRI;
-import fr.inria.corese.core.next.data.api.term.SimpleInteger;
-import fr.inria.corese.core.next.data.api.term.SimpleLiteral;
-import fr.inria.corese.core.next.data.api.term.SimpleTime;
-import fr.inria.corese.core.next.data.api.term.SimpleTriple;
-import fr.inria.corese.core.next.data.api.term.Triple;
import fr.inria.corese.core.next.data.api.term.Value;
-import fr.inria.corese.core.next.storage.api.config.StorageConfig;
import fr.inria.corese.core.next.storage.api.exception.ErrorCode;
import fr.inria.corese.core.next.storage.api.exception.StorageException;
import fr.inria.corese.core.next.storage.api.lifecycle.LifecycleState;
@@ -31,7 +14,6 @@
import fr.inria.corese.core.next.storage.api.operations.MetadataOperations;
import fr.inria.corese.core.next.storage.api.operations.MutationOperations;
import fr.inria.corese.core.next.storage.api.operations.QueryOperations;
-import fr.inria.corese.core.next.storage.api.operations.StatementSpliterator;
import fr.inria.corese.core.next.storage.api.transaction.IsolationLevel;
import fr.inria.corese.core.next.storage.api.transaction.Transaction;
import fr.inria.corese.core.next.storage.api.transaction.TransactionManager;
@@ -41,18 +23,13 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
-import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.Optional;
import java.util.Set;
-import java.util.Spliterator;
-import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
-import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
@@ -62,7 +39,7 @@
* Session-scoped operations over a shared, partitioned in-memory database.
*
*
- * The database monitor protects publication, transactions and partition ownership counts. Each live {@code State} owns
+ * The database monitor protects publication, transactions and partition ownership counts. Each live {@code PartitionState} owns
* one reference to each partition in its graph registry. A transaction or lazy read captures its own state and
* increments those references. Before mutating a shared partition, an owner copies all three index containers and
* transfers its reference to the private copy. Immutable statements and term keys remain shared. CLEAR replaces a
@@ -93,7 +70,7 @@ final class IndexedStatementStore
/**
* Shared lifecycle monitor and committed database state.
*/
- private final Database database;
+ final MemoryDatabase database;
/**
* Active transaction owned by this session, or null.
*/
@@ -107,380 +84,11 @@ final class IndexedStatementStore
*/
private boolean loading;
- /** One graph and its three permutations; references count State owners, not rows. */
- private static final class Partition {
- // Number of live States owning this partition, guarded by the database monitor.
- /**
- * Number of live State owners, guarded by the database monitor.
- */
- private int references = 1;
- /**
- * Stable graph registry position.
- */
- private final long ordinal;
- /**
- * Canonical graph identifier; null denotes the default graph.
- */
- private final Resource context;
- /**
- * Subject/predicate/object permutation of this graph.
- */
- private final TripleIndex spo;
- /**
- * Predicate/object/subject permutation of this graph.
- */
- private final TripleIndex pos;
- /**
- * Object/subject/predicate permutation of this graph.
- */
- private final TripleIndex osp;
-
- /**
- * Creates a graph partition owning the supplied permutations or empty indexes.
- *
- * @param context canonical graph identifier; null denotes the default graph
- * @param ordinal stable graph registry ordinal
- */
- private Partition(final Resource context, final long ordinal) {
- this(context, ordinal, new TripleIndex(TripleIndex.Order.SPO), new TripleIndex(TripleIndex.Order.POS),
- new TripleIndex(TripleIndex.Order.OSP));
- }
-
- /**
- * Creates a graph partition owning the supplied permutations or empty indexes.
- *
- * @param context canonical graph identifier; null denotes the default graph
- * @param ordinal stable graph registry ordinal
- * @param spo SPO index owning immutable statements and canonical keys
- * @param pos POS permutation of the same graph partition
- * @param osp OSP permutation of the same graph partition
- */
- private Partition(final Resource context, final long ordinal, final TripleIndex spo, final TripleIndex pos,
- final TripleIndex osp) {
- this.context = context;
- this.ordinal = ordinal;
- this.spo = spo;
- this.pos = pos;
- this.osp = osp;
- }
-
- /**
- * Constructs a complete partition and releases its SPO staging dictionary before publication.
- *
- * @param spo SPO index owning immutable statements and canonical keys
- * @param context canonical graph identifier; null denotes the default graph
- * @param ordinal stable graph registry ordinal
- * @return a complete graph partition with no retained staging dictionary
- */
- private static Partition build(final TripleIndex spo, final Resource context, final long ordinal) {
- final TripleIndex.SecondaryIndexes secondary = TripleIndex.buildSecondaryIndexes(spo);
- final Partition partition = new Partition(context, ordinal, spo, secondary.pos(), secondary.osp());
- spo.finishStaging();
- return partition;
- }
-
- /**
- * Copies owned mutable containers while sharing immutable keys and statements.
- *
- * @return an independently owned copy of the mutable containers
- */
- private Partition copy() {
- return new Partition(context, ordinal, spo.copy(), pos.copy(), osp.copy());
- }
-
- /**
- * Adds a statement or keyed value if it is absent.
- *
- * @param s captured statement to index
- * @return true when an absent entry was added, false when it was already present
- */
- private boolean add(Statement s) {
- final TermKey subject = key(s.getSubject(), null, null, spo, pos, osp);
- final TermKey predicate = key(s.getPredicate(), subject, null, pos, spo, osp);
- final TermKey object = key(s.getObject(), subject, predicate, osp, spo, pos);
- s = sharedStatement(s, subject, predicate, object, context);
- if (!spo.add(s, subject, predicate, object, osp)) {
- return false;
- }
- pos.add(s, subject, predicate, object, spo);
- osp.add(s, subject, predicate, object, pos);
- return true;
- }
-
- /**
- * Reuses a canonical RDF term key across roles, creating one only for a previously unseen term.
- *
- * @param value non-null RDF value or entry value
- * @param subject canonical subject key, or null for a wildcard search
- * @param predicate canonical predicate key; null denotes a wildcard for search operations
- * @param primary preferred role-root index for canonical lookup
- * @param secondary second role-root index for canonical lookup
- * @param tertiary third role-root index for canonical lookup
- * @return a reusable canonical key, or a newly captured key for an unseen RDF term
- */
- private TermKey key(final Value value, final TermKey subject, final TermKey predicate,
- final TripleIndex primary, final TripleIndex secondary, final TripleIndex tertiary) {
- final int hash = TermKey.structuralHash(value);
- if (subject != null && subject.matchesLookup(value, hash)) {
- return subject;
- }
- if (predicate != null && predicate.matchesLookup(value, hash)) {
- return predicate;
- }
- // Role roots retain canonical keys; absent RDF terms require a new key.
- TermKey canonical = primary.existingKey(value, hash);
- if (canonical == null) {
- canonical = secondary.existingKey(value, hash);
- }
- if (canonical == null) {
- canonical = tertiary.existingKey(value, hash);
- }
- return canonical == null ? new TermKey(value, hash) : canonical;
- }
-
- /**
- * Removes matching content while preserving the order of survivors.
- *
- * @param s captured statement to index
- * @return true when an entry was removed, false when it was absent
- */
- private boolean remove(final Statement s) {
- final TermKey subject = new TermKey(s.getSubject());
- final TermKey predicate = new TermKey(s.getPredicate());
- final TermKey object = new TermKey(s.getObject());
- if (!spo.remove(subject, predicate, object)) {
- return false;
- }
- pos.remove(subject, predicate, object);
- osp.remove(subject, predicate, object);
- return true;
- }
-
- /**
- * Removes selected index content with stable filtering and unchanged compaction rules.
- *
- * @param pattern captured search pattern
- */
- private void removeMatches(final StatementPattern pattern) {
- final TermKey subject = TermKey.nullable(pattern.getSubject());
- final TermKey predicate = TermKey.nullable(pattern.getPredicate());
- final TermKey object = TermKey.nullable(pattern.getObject());
- spo.removeMatches(subject, predicate, object);
- pos.removeMatches(subject, predicate, object);
- osp.removeMatches(subject, predicate, object);
- }
-
- /**
- * Selects the permutation appropriate for the bound prefix of a search pattern.
- *
- * @param p captured search pattern
- * @return the SPO, POS or OSP permutation selected for this pattern
- */
- private TripleIndex index(final StatementPattern p) {
- if (p.getSubject() != null && (p.getPredicate() != null || p.getObject() == null)) {
- return spo;
- }
- if (p.getPredicate() != null) {
- return pos;
- }
- return p.getObject() != null ? osp : spo;
- }
-
- /**
- * Reports whether the session-visible or retained graph content contains a match.
- *
- * @param statement statement whose RDF identities have been captured
- * @return true when at least one matching statement exists
- */
- private boolean contains(final Statement statement) {
- return spo.count(
- StatementPattern.of(statement.getSubject(), statement.getPredicate(), statement.getObject())) != 0;
- }
- }
-
- /** Ordered graph registry whose partition references are acquired and released together. */
- private static final class State {
-
- /** Creates an empty ordered registry with exclusive ownership of its map. */
- private State() {
- }
-
- // A null key is the default partition. Empty named partitions are retained until DROP.
- /**
- * Ordered partition registry; empty named graphs are retained until DROP.
- */
- private final Map graphs = new LinkedHashMap<>();
- /**
- * Number of live entries or statements owned by this container.
- */
- private long size;
- /**
- * Ordinal assigned to the next created graph.
- */
- private long nextOrdinal;
-
- /**
- * Copies owned mutable containers while sharing immutable keys and statements.
- *
- * @return an independently owned copy of the mutable containers
- */
- private State copy() {
- final State copy = new State();
- graphs.forEach((graph, partition) -> {
- partition.references++;
- copy.graphs.put(graph, partition);
- });
- copy.size = size;
- copy.nextOrdinal = nextOrdinal;
- return copy;
- }
-
- /**
- * Capture selected graphs only; writes to unrelated graphs need no cursor-induced COW.
- *
- * @param contexts selected graph identifiers; an empty array selects all graphs
- * @return an independently owned copy of the mutable containers
- */
- private State copy(final Resource[] contexts) {
- if (contexts.length == 0) return copy();
- final State copy = new State();
- final List selected = new ArrayList<>();
- for (final Resource graph : contexts) {
- final Partition partition = graphs.get(graph);
- if (partition != null) selected.add(partition);
- }
- selected.sort(Comparator.comparingLong(partition -> partition.ordinal));
- for (final Partition partition : selected) {
- partition.references++;
- copy.graphs.put(partition.context, partition);
- copy.size += partition.spo.size();
- }
- copy.nextOrdinal = nextOrdinal;
- return copy;
- }
-
- /**
- * Detaches only a shared partition, preserving existing readers and graph order.
- *
- * @param graph named graph identifier or staging registry key, according to the operation
- * @return the exclusively owned partition, or null if the graph is absent
- */
- private Partition writable(final Resource graph) {
- final Partition partition = graphs.get(graph);
- if (partition != null && partition.references > 1) {
- final Partition replacement = partition.copy();
- partition.references--;
- graphs.put(graph, replacement);
- return replacement;
- }
- return partition;
- }
-
- /**
- * Replaces a partition with an empty version, preserving graph identity and registry position.
- *
- * @param partition partition retained or exclusively owned by the supplied state
- */
- private void empty(final Partition partition) {
- // Keep the graph's identity and registry position. Readers continue to own
- // the old partition; clearing never needs to clone its three indexes.
- final Partition replacement = new Partition(partition.context, partition.ordinal);
- graphs.put(partition.context, replacement);
- partition.references--;
- }
-
- /** Releases this state's ownership; clearing the registry makes repeated release harmless. */
- private void release() {
- graphs.values().forEach(partition -> partition.references--);
- graphs.clear();
- }
- }
-
- /**
- * Shared database state and lifecycle monitor for all sessions.
- */
- private static final class Database implements StorageLifecycle {
-
- /** Creates an uninitialized database and its exclusively owned empty committed state. */
- private Database() {
- }
-
- /**
- * Committed state whose partition references are owned by the database.
- */
- private State committed = new State();
- /**
- * Committed version captured for optimistic conflict detection.
- */
- private long version;
- /**
- * Current shared database lifecycle status.
- */
- private LifecycleState lifecycleState = LifecycleState.NOT_INITIALIZED;
- /**
- * Configuration available during the running lifecycle.
- */
- private StorageConfig config;
-
- /**
- * {@inheritDoc}
- *
- * @param config storage configuration selecting this backend
- * @throws IllegalStateException if the required representation, ownership or lifecycle state is not satisfied
- * @throws NullPointerException if an explicitly required argument or mapped value is null
- */
- @Override
- public synchronized void initialize(final StorageConfig config) {
- Objects.requireNonNull(config, "config");
- if (lifecycleState != LifecycleState.NOT_INITIALIZED) {
- throw new IllegalStateException("Already initialized");
- }
- this.config = config;
- lifecycleState = LifecycleState.RUNNING;
- }
-
- /**
- * {@inheritDoc}
- *
- * @throws IllegalStateException if the required representation, ownership or lifecycle state is not satisfied
- */
- @Override
- public synchronized void shutdown() {
- if (lifecycleState != LifecycleState.RUNNING) {
- throw new IllegalStateException("Storage is not running");
- }
- committed.release();
- committed = new State();
- config = null;
- lifecycleState = LifecycleState.SHUTDOWN;
- }
-
- /**
- * {@inheritDoc}
- *
- * @return the current lifecycle or transaction status
- */
- @Override
- public synchronized LifecycleState getState() {
- return lifecycleState;
- }
-
- /**
- * {@inheritDoc}
- *
- * @return current configuration when the database is running
- */
- @Override
- public synchronized Optional getConfig() {
- return Optional.ofNullable(config);
- }
- }
-
/**
* Creates session operations sharing the supplied database, or a new uninitialized database.
*/
IndexedStatementStore() {
- this(new Database());
+ this(new MemoryDatabase());
}
/**
@@ -488,7 +96,7 @@ public synchronized Optional getConfig() {
*
* @param database shared database monitor and state
*/
- private IndexedStatementStore(final Database database) {
+ private IndexedStatementStore(final MemoryDatabase database) {
this.database = database;
}
@@ -531,9 +139,9 @@ IndexedStatementStore openReadSnapshot() {
synchronized (database) {
checkAccess();
final IndexedStatementStore snapshot = new IndexedStatementStore(database);
- final State captured = state().copy();
+ final PartitionState captured = state().copy();
try {
- snapshot.transaction = snapshot.new SnapshotTransaction(captured, getDataVersion(), true);
+ snapshot.transaction = new SnapshotTransaction(database, snapshot, captured, getDataVersion(), true);
return snapshot;
} catch (final RuntimeException | Error failure) {
captured.release();
@@ -566,12 +174,20 @@ void closeSession() {
}
}
+ boolean isLoading() {
+ return loading;
+ }
+
+ void clearTransaction() {
+ this.transaction = null;
+ }
+
/**
* Requires an open session and, when active, access from its transaction owner thread.
*
* @throws IllegalStateException if the required representation, ownership or lifecycle state is not satisfied
*/
- private void checkAccess() {
+ void checkAccess() {
if (!isOpen()) {
throw new IllegalStateException("Storage session is closed");
}
@@ -600,7 +216,7 @@ private void checkWrite() {
*
* @return the transaction state, or the committed state outside a transaction
*/
- private State state() {
+ PartitionState state() {
return transaction == null ? database.committed : transaction.state;
}
@@ -630,171 +246,6 @@ public long getDataVersion() {
}
}
- /**
- * Captures RDF identities without changing the statement or pattern semantics.
- *
- * @param value non-null RDF value or entry value
- * @return the original supported native value or a captured representation of its RDF identity
- * @throws IllegalArgumentException if a capacity, term type or requested isolation level is unsupported
- * @throws NullPointerException if an explicitly required argument or mapped value is null
- */
- private static Value immutable(final Value value) {
- Objects.requireNonNull(value, "value");
- if (value instanceof Literal literal && !(literal.getDatatype() instanceof SimpleIRI)) {
- final IRI datatype = (IRI) immutable(literal.getDatatype());
- if (value instanceof SimpleLiteral) {
- return literal.getLanguage().map(language -> (Value) new SimpleLiteral(literal.getLabel(), language))
- .orElseGet(() -> new SimpleLiteral(literal.getLabel(), datatype));
- }
- return Values.factory().createLiteral(literal.getLabel(), datatype);
- }
- if (value instanceof SimpleIRI || value instanceof SimpleBNode || value instanceof SimpleLiteral
- || value instanceof SimpleInteger || value instanceof SimpleDecimal || value instanceof SimpleDouble
- || value instanceof SimpleDate || value instanceof SimpleDateTime || value instanceof SimpleTime
- || value instanceof SimpleDuration) {
- return value;
- }
- if (value instanceof IRI iri) {
- return new SimpleIRI(iri.stringValue());
- }
- if (value instanceof BNode blank) {
- return new SimpleBNode(blank.getID());
- }
- if (value instanceof Literal literal) {
- return literal.getLanguage().map(language -> (Value) new SimpleLiteral(literal.getLabel(), language))
- .orElseGet(() -> new SimpleLiteral(literal.getLabel(), (IRI) immutable(literal.getDatatype())));
- }
- if (value instanceof Triple triple) {
- return new SimpleTriple((Resource) immutable(triple.subject()), (IRI) immutable(triple.predicate()),
- immutable(triple.object()));
- }
- throw new IllegalArgumentException("Unsupported RDF term: " + value.getClass().getName());
- }
-
- /**
- * Captures RDF identities without changing the statement or pattern semantics.
- *
- * @param statement statement whose RDF identities have been captured
- * @return the original supported native value or a captured representation of its RDF identity
- * @throws NullPointerException if an explicitly required argument or mapped value is null
- */
- private static Statement immutable(final Statement statement) {
- Objects.requireNonNull(statement, "statement");
- final Resource context = statement.getContext();
- final Resource subject = (Resource) immutable(statement.getSubject());
- final IRI predicate = (IRI) immutable(statement.getPredicate());
- final Value object = immutable(statement.getObject());
- final Resource graph = context == null ? null : (Resource) immutable(context);
- if (statement.getClass() == SimpleStatement.class && subject == statement.getSubject()
- && predicate == statement.getPredicate() && object == statement.getObject() && graph == context) {
- return statement;
- }
- return new SimpleStatement(subject, predicate, object, graph);
- }
-
- /**
- * Share safe native terms without changing lexical identity or value implementation.
- *
- * @param statement statement whose RDF identities have been captured
- * @param subject canonical subject key, or null for a wildcard search
- * @param predicate canonical predicate key; null denotes a wildcard for search operations
- * @param object canonical object key, or null for a wildcard search
- * @return the original statement when all values are already shared, otherwise a statement using compatible
- * canonical values
- */
- static Statement sharedStatement(final Statement statement, final TermKey subject, final TermKey predicate,
- final TermKey object) {
- return sharedStatement(statement, subject, predicate, object, statement.getContext());
- }
-
- /**
- * Reuses compatible canonical RDF values without changing lexical identity or implementation type.
- *
- * @param statement statement whose RDF identities have been captured
- * @param subject canonical subject key, or null for a wildcard search
- * @param predicate canonical predicate key; null denotes a wildcard for search operations
- * @param object canonical object key, or null for a wildcard search
- * @param context canonical graph identifier; null denotes the default graph
- * @return the original statement when all values are already shared, otherwise a statement using compatible
- * canonical values
- */
- static Statement sharedStatement(final Statement statement, final TermKey subject, final TermKey predicate,
- final TermKey object, final Resource context) {
- final Resource s = (Resource) sharedValue(statement.getSubject(), subject.value());
- final IRI p = (IRI) sharedValue(statement.getPredicate(), predicate.value());
- final Value o = sharedValue(statement.getObject(), object.value());
- final Resource g = context == null ? null : (Resource) sharedValue(statement.getContext(), context);
- return s == statement.getSubject() && p == statement.getPredicate() && o == statement.getObject()
- && g == statement.getContext() ? statement : new SimpleStatement(s, p, o, g);
- }
-
- /**
- * Reuses a compatible immutable scalar value while preserving its implementation and datatype.
- *
- * @param original original captured RDF value
- * @param canonical compatible canonical RDF value
- * @return the compatible canonical value, or the original value when pooling is unsuitable
- */
- private static Value sharedValue(final Value original, final Value canonical) {
- if (original == canonical || original.getClass() != canonical.getClass()) {
- return original;
- }
- if (original instanceof Literal literal && canonical instanceof Literal other
- && !sameLiteralImplementation(literal, other)) {
- return original;
- }
- if (original instanceof SimpleIRI iri && canonical instanceof SimpleIRI other) {
- return iri.getNamespace().equals(other.getNamespace()) && iri.getLocalName().equals(other.getLocalName())
- ? canonical
- : original;
- }
- if (original instanceof SimpleLiteral literal && canonical instanceof SimpleLiteral other) {
- return literal.getCoreDatatype() == other.getCoreDatatype() ? canonical : original;
- }
- // Calendar-backed terms and nested triples are deliberately not pooled.
- return original instanceof SimpleBNode || original instanceof SimpleInteger || original instanceof SimpleDecimal
- || original instanceof SimpleDouble || original instanceof SimpleDuration ? canonical : original;
- }
-
- /**
- * Checks literal datatype implementation compatibility using the existing comparison order.
- *
- * @param literal original captured literal
- * @param other value or search object to compare with the stored key
- * @return true when datatype implementation and native components permit sharing
- */
- private static boolean sameLiteralImplementation(final Literal literal, final Literal other) {
- if (literal.getCoreDatatype() != other.getCoreDatatype()) {
- return false;
- }
- final IRI datatype = literal.getDatatype();
- final IRI canonicalDatatype = other.getDatatype();
-
- return datatype == canonicalDatatype || (datatype.getClass() == canonicalDatatype.getClass()
- && datatype.getNamespace().equals(canonicalDatatype.getNamespace())
- && datatype.getLocalName().equals(canonicalDatatype.getLocalName()));
- }
-
- /**
- * Captures RDF identities without changing the statement or pattern semantics.
- *
- * @param pattern captured search pattern
- * @return the original supported native value or a captured representation of its RDF identity
- * @throws NullPointerException if an explicitly required argument or mapped value is null
- */
- private static StatementPattern immutable(final StatementPattern pattern) {
- Objects.requireNonNull(pattern, "pattern");
- final Resource[] contexts = pattern.getContexts();
- for (int i = 0; i < contexts.length; i++) {
- if (contexts[i] != null) {
- contexts[i] = (Resource) immutable(contexts[i]);
- }
- }
- return StatementPattern.of(pattern.getSubject() == null ? null : (Resource) immutable(pattern.getSubject()),
- pattern.getPredicate() == null ? null : (IRI) immutable(pattern.getPredicate()),
- pattern.getObject() == null ? null : immutable(pattern.getObject()), contexts);
- }
-
/**
* Captures the selected partitions immediately and traverses matches lazily.
*
@@ -815,12 +266,12 @@ private static StatementPattern immutable(final StatementPattern pattern) {
@SuppressWarnings("java:S1181")
@Override
public Stream find(StatementPattern pattern) {
- pattern = immutable(pattern);
+ pattern = StatementCapture.immutable(pattern);
synchronized (database) {
checkAccess();
- final State snapshot = state().copy(pattern.getContexts());
+ final PartitionState snapshot = state().copy(pattern.getContexts());
try {
- var cursor = new SnapshotCursor(snapshot, pattern);
+ var cursor = new SnapshotStatementSpliterator(database, snapshot, pattern);
return StreamSupport.stream(cursor, false).onClose(cursor::close);
} catch (final RuntimeException | Error failure) {
snapshot.release();
@@ -829,222 +280,6 @@ public Stream find(StatementPattern pattern) {
}
}
- /**
- * Single-consumer cursor owning a captured state until close, exhaustion or failure. Known-size traversal can
- * release ownership before delivering the last immutable statement. Close is idempotent and clears traversal
- * references under the monitor. No database monitor is held while visiting rows or invoking consumer code. Session
- * and transaction closure do not invalidate this independent read snapshot.
- */
- private final class SnapshotCursor implements StatementSpliterator, AutoCloseable {
- /**
- * Read state retained until this cursor is closed or exhausted.
- */
- private State snapshot;
- /**
- * Ordered traversal of the retained partition registry.
- */
- private Iterator partitions;
- /**
- * Positional traversal of the current retained partition.
- */
- private TripleIndex.Cursor rows;
- /**
- * Captured immutable search pattern.
- */
- private final StatementPattern pattern;
- /**
- * Canonical traversal subject or current batch subject, according to the enclosing traversal.
- */
- private final TermKey subject;
- /**
- * Canonical traversal predicate or current batch predicate, according to the enclosing traversal.
- */
- private final TermKey predicate;
- /**
- * Captured object search key; null denotes a wildcard.
- */
- private final TermKey object;
- /**
- * Whether the retained match count is known exactly.
- */
- private final boolean sized;
- /**
- * Snapshot-bound guarantee of SPO uniqueness independently of context.
- */
- private final boolean distinctTriples;
- /**
- * Remaining exact count or the unsized upper bound while this cursor is open.
- */
- private long remaining;
-
- /**
- * Creates a single-consumer cursor owning the supplied retained read state.
- *
- * @param snapshot retained read state
- * @param pattern captured search pattern
- */
- private SnapshotCursor(final State snapshot, final StatementPattern pattern) {
- this.snapshot = snapshot;
- this.pattern = pattern;
- distinctTriples = hasAtMostOnePopulatedPartition(snapshot);
- subject = TermKey.nullable(pattern.getSubject());
- predicate = TermKey.nullable(pattern.getPredicate());
- object = TermKey.nullable(pattern.getObject());
- sized = subject == null && predicate == null && object == null;
- remaining = sized ? snapshot.size : Long.MAX_VALUE;
- partitions = snapshot.graphs.values().iterator();
- }
-
- /**
- * {@inheritDoc}
- *
- * @param action consumer invoked outside the database monitor
- * @return true when a statement was delivered, false when this cursor is closed or exhausted
- * @throws NullPointerException if an explicitly required argument or mapped value is null
- */
- // Traversal Errors release ownership and are rethrown unchanged.
- @SuppressWarnings("java:S1181")
- @Override
- public boolean tryAdvance(final Consumer super Statement> action) {
- Objects.requireNonNull(action);
- if (snapshot == null) {
- return false;
- }
- try {
- for (;;) {
- final Statement statement = rows == null ? null : rows.next();
- if (statement != null) {
- if (sized && --remaining == 0) {
- close();
- }
- action.accept(statement);
- return true;
- }
- if (!partitions.hasNext()) {
- close();
- return false;
- }
- rows = partitions.next().index(pattern).cursor(subject, predicate, object);
- }
- } catch (final RuntimeException | Error failure) {
- close();
- throw failure;
- }
- }
-
- /**
- * {@inheritDoc}
- *
- * @param action consumer invoked outside the database monitor
- * @throws NullPointerException if an explicitly required argument or mapped value is null
- */
- @Override
- public void forEachRemaining(final Consumer super Statement> action) {
- Objects.requireNonNull(action);
- if (snapshot == null) {
- return;
- }
- // A complete scan uses the existing bulk visitor, without per-row cursor overhead.
- if (rows != null) {
- StatementSpliterator.super.forEachRemaining(action);
- return;
- }
- try {
- final Predicate visitor = statement -> {
- if (sized && --remaining == 0) {
- close();
- }
- action.accept(statement);
- return snapshot == null;
- };
- while (snapshot != null && partitions.hasNext()) {
- partitions.next().index(pattern).visit(subject, predicate, object, visitor);
- }
- } finally {
- close();
- }
- }
-
- /**
- * {@inheritDoc}
- *
- * @return true only when SPO uniqueness is guaranteed for this retained read
- */
- @Override
- public boolean hasDistinctTriples() {
- return distinctTriples;
- }
-
- /**
- * {@inheritDoc}
- *
- * @return null; this traversal cannot be split
- */
- @Override
- public Spliterator trySplit() {
- return null;
- }
-
- /**
- * {@inheritDoc}
- *
- * @return remaining size estimate, or zero once the cursor is closed
- */
- @Override
- public long estimateSize() {
- return snapshot == null ? 0 : remaining;
- }
-
- /**
- * {@inheritDoc}
- *
- * @return the ORDERED, NONNULL and IMMUTABLE flags, plus SIZED and SUBSIZED when the exact count is known
- */
- @Override
- public int characteristics() {
- return ORDERED | NONNULL | IMMUTABLE | (sized ? SIZED | SUBSIZED : 0);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void close() {
- synchronized (database) {
- if (snapshot == null) {
- return;
- }
- snapshot.release();
- snapshot = null;
- partitions = null;
- rows = null;
- }
- }
- }
-
- /**
- * Empty named partitions survive CLEAR but cannot contribute duplicate triples.
- *
- * @param snapshot retained read state
- * @return true when at most one selected partition contains statements
- */
- private static boolean hasAtMostOnePopulatedPartition(final State snapshot) {
- if (snapshot.graphs.size() <= 1) {
- return true;
- }
- boolean populated = false;
- for (final Partition partition : snapshot.graphs.values()) {
- if (partition.spo.size() == 0) {
- continue;
- }
- if (populated) {
- return false;
- }
- populated = true;
- }
- return true;
- }
-
/**
* {@inheritDoc}
*
@@ -1054,24 +289,24 @@ private static boolean hasAtMostOnePopulatedPartition(final State snapshot) {
*/
@Override
public long count(StatementPattern pattern) {
- pattern = immutable(pattern);
+ pattern = StatementCapture.immutable(pattern);
synchronized (database) {
checkAccess();
if (pattern.matchesAll()) {
return state().size;
}
- final State current = state();
+ final PartitionState current = state();
if (current.graphs.isEmpty()) {
return 0;
}
final Resource[] contexts = pattern.getContexts();
// Keep one-partition keys local to the index for JVM scalar replacement.
if (contexts.length == 1) {
- final Partition partition = current.graphs.get(contexts[0]);
+ final GraphPartition partition = current.graphs.get(contexts[0]);
return partition == null ? 0 : partition.index(pattern).count(pattern);
}
if (contexts.length == 0 && current.graphs.size() == 1) {
- final Partition partition = current.graphs.values().iterator().next();
+ final GraphPartition partition = current.graphs.values().iterator().next();
return partition.index(pattern).count(pattern);
}
return countSelectedPartitions(current, contexts, pattern);
@@ -1086,7 +321,7 @@ public long count(StatementPattern pattern) {
* @param pattern captured search pattern
* @return number of matching statements across selected graph partitions
*/
- private static long countSelectedPartitions(final State current, final Resource[] contexts,
+ private static long countSelectedPartitions(final PartitionState current, final Resource[] contexts,
final StatementPattern pattern) {
// Call-local keys: no per-partition hashing or retained query cache.
final TermKey subject = TermKey.nullable(pattern.getSubject());
@@ -1094,12 +329,12 @@ private static long countSelectedPartitions(final State current, final Resource[
final TermKey object = TermKey.nullable(pattern.getObject());
long count = 0;
if (contexts.length == 0) {
- for (final Partition partition : current.graphs.values()) {
+ for (final GraphPartition partition : current.graphs.values()) {
count += partition.index(pattern).count(subject, predicate, object);
}
} else {
for (final Resource context : contexts) {
- final Partition partition = current.graphs.get(context);
+ final GraphPartition partition = current.graphs.get(context);
if (partition != null) {
count += partition.index(pattern).count(subject, predicate, object);
}
@@ -1117,7 +352,7 @@ private static long countSelectedPartitions(final State current, final Resource[
*/
@Override
public boolean contains(StatementPattern pattern) {
- pattern = immutable(pattern);
+ pattern = StatementCapture.immutable(pattern);
synchronized (database) {
checkAccess();
return visit(state(), pattern, s -> true);
@@ -1132,18 +367,18 @@ public boolean contains(StatementPattern pattern) {
* @param visitor non-mutating callback; true requests an early stop for predicate visitors
* @return true when a matching value caused the visitor to request an early stop
*/
- private static boolean visit(final State state, final StatementPattern pattern,
+ private static boolean visit(final PartitionState state, final StatementPattern pattern,
final Predicate visitor) {
if (state.graphs.isEmpty()) {
return false;
}
final Resource[] contexts = pattern.getContexts();
if (contexts.length == 1) {
- final Partition partition = state.graphs.get(contexts[0]);
+ final GraphPartition partition = state.graphs.get(contexts[0]);
return partition != null && partition.index(pattern).visit(pattern, visitor);
}
if (contexts.length == 0 && state.graphs.size() == 1) {
- final Partition partition = state.graphs.values().iterator().next();
+ final GraphPartition partition = state.graphs.values().iterator().next();
return partition.index(pattern).visit(pattern, visitor);
}
return visitSelectedPartitions(state, pattern, visitor, contexts);
@@ -1158,20 +393,20 @@ private static boolean visit(final State state, final StatementPattern pattern,
* @param contexts selected graph identifiers; an empty array selects all graphs
* @return true when the visitor requests an early stop on a matching statement
*/
- private static boolean visitSelectedPartitions(final State state, final StatementPattern pattern,
+ private static boolean visitSelectedPartitions(final PartitionState state, final StatementPattern pattern,
final Predicate visitor, final Resource[] contexts) {
final TermKey subject = TermKey.nullable(pattern.getSubject());
final TermKey predicate = TermKey.nullable(pattern.getPredicate());
final TermKey object = TermKey.nullable(pattern.getObject());
if (contexts.length == 0) {
- for (final Partition partition : state.graphs.values()) {
+ for (final GraphPartition partition : state.graphs.values()) {
if (partition.index(pattern).visit(subject, predicate, object, visitor)) {
return true;
}
}
} else {
- final List partitions = selectPartitions(state, contexts);
- for (final Partition partition : partitions) {
+ final List partitions = selectPartitions(state, contexts);
+ for (final GraphPartition partition : partitions) {
if (partition.index(pattern).visit(subject, predicate, object, visitor)) {
return true;
}
@@ -1187,11 +422,11 @@ private static boolean visitSelectedPartitions(final State state, final Statemen
* @param contexts selected graph identifiers; an empty array selects all graphs
* @return existing selected partitions in stable registry order
*/
- private static List selectPartitions(final State state, final Resource[] contexts) {
+ private static List selectPartitions(final PartitionState state, final Resource[] contexts) {
// Preserve registry order independently of the caller's context ordering.
- final List partitions = new ArrayList<>();
+ final List partitions = new ArrayList<>();
for (final Resource context : contexts) {
- final Partition partition = state.graphs.get(context);
+ final GraphPartition partition = state.graphs.get(context);
if (partition != null) {
partitions.add(partition);
}
@@ -1210,17 +445,17 @@ private static List selectPartitions(final State state, final Resourc
*/
@Override
public boolean add(final Statement statement) {
- final Statement captured = immutable(statement);
+ final Statement captured = StatementCapture.immutable(statement);
synchronized (database) {
checkWrite();
- final State current = state();
- final Partition existing = current.graphs.get(captured.getContext());
+ final PartitionState current = state();
+ final GraphPartition existing = current.graphs.get(captured.getContext());
if (existing != null && existing.references > 1 && existing.contains(captured)) {
return false;
}
- Partition partition = current.writable(captured.getContext());
+ GraphPartition partition = current.writable(captured.getContext());
if (partition == null) {
- partition = new Partition(captured.getContext(), current.nextOrdinal++);
+ partition = new GraphPartition(captured.getContext(), current.nextOrdinal++);
current.graphs.put(captured.getContext(), partition);
}
final boolean added = partition.add(captured);
@@ -1242,14 +477,14 @@ public boolean add(final Statement statement) {
*/
@Override
public boolean remove(final Statement statement) {
- final Statement captured = immutable(statement);
+ final Statement captured = StatementCapture.immutable(statement);
synchronized (database) {
checkWrite();
- final Partition existing = state().graphs.get(captured.getContext());
+ final GraphPartition existing = state().graphs.get(captured.getContext());
if (existing == null || (existing.references > 1 && !existing.contains(captured))) {
return false;
}
- final Partition partition = state().writable(captured.getContext());
+ final GraphPartition partition = state().writable(captured.getContext());
final boolean removed = partition != null && partition.remove(captured);
if (removed) {
state().size--;
@@ -1269,20 +504,20 @@ public boolean remove(final Statement statement) {
*/
@Override
public long remove(StatementPattern pattern) {
- pattern = immutable(pattern);
+ pattern = StatementCapture.immutable(pattern);
synchronized (database) {
checkWrite();
- final State current = state();
+ final PartitionState current = state();
final Resource[] contexts = pattern.getContexts();
long removed = 0;
if (contexts.length == 0) {
// Replacing an existing value does not change LinkedHashMap iteration order.
- for (final Partition partition : current.graphs.values()) {
+ for (final GraphPartition partition : current.graphs.values()) {
removed += remove(current, partition, pattern);
}
} else {
for (final Resource context : contexts) {
- final Partition partition = current.graphs.get(context);
+ final GraphPartition partition = current.graphs.get(context);
if (partition != null) {
removed += remove(current, partition, pattern);
}
@@ -1304,7 +539,7 @@ public long remove(StatementPattern pattern) {
* @throws IllegalStateException if the session is closed or its transaction belongs to another thread, or a
* conflicting batch is active
*/
- private static long remove(final State current, final Partition partition, final StatementPattern pattern) {
+ private static long remove(final PartitionState current, final GraphPartition partition, final StatementPattern pattern) {
final long size = partition.spo.size();
if (size == 0) {
return 0;
@@ -1326,7 +561,7 @@ private static long remove(final State current, final Partition partition, final
matches.add(s);
return false;
});
- final Partition writable = current.writable(partition.context);
+ final GraphPartition writable = current.writable(partition.context);
for (final Statement statement : matches) {
writable.remove(statement);
}
@@ -1358,13 +593,13 @@ public long clear(final Resource... contexts) {
*/
@Override
public boolean createGraph(final Resource graph) {
- final Resource captured = (Resource) immutable(Objects.requireNonNull(graph, "graph"));
+ final Resource captured = (Resource) StatementCapture.immutable(Objects.requireNonNull(graph, "graph"));
synchronized (database) {
checkWrite();
if (state().graphs.containsKey(captured)) {
return false;
}
- state().graphs.put(captured, new Partition(captured, state().nextOrdinal++));
+ state().graphs.put(captured, new GraphPartition(captured, state().nextOrdinal++));
changed(true);
return true;
}
@@ -1381,10 +616,10 @@ public boolean createGraph(final Resource graph) {
*/
@Override
public boolean dropGraph(final Resource graph) {
- final Resource captured = (Resource) immutable(Objects.requireNonNull(graph, "graph"));
+ final Resource captured = (Resource) StatementCapture.immutable(Objects.requireNonNull(graph, "graph"));
synchronized (database) {
checkWrite();
- final Partition removed = state().graphs.remove(captured);
+ final GraphPartition removed = state().graphs.remove(captured);
if (removed == null) {
return false;
}
@@ -1437,7 +672,7 @@ private static Map stageStatements(final Iterable e
final Function newPartition = graph -> new StagedPartition(graph,
TripleIndex.forStaging(staged.isEmpty() ? expectedTerms : 0));
for (final Statement statement : statements) {
- final Statement captured = immutable(statement);
+ final Statement captured = StatementCapture.immutable(statement);
final StagedPartition partition = staged.computeIfAbsent(captured.getContext(), newPartition);
partition.statements.add(captured, partition.context);
}
@@ -1453,8 +688,8 @@ private static Map stageStatements(final Iterable e
private long publishBatch(final Map staged) {
synchronized (database) {
checkAccess();
- final State target = state();
- final State replacement = target.copy();
+ final PartitionState target = state();
+ final PartitionState replacement = target.copy();
boolean published = false;
try {
final long[] added = { 0 };
@@ -1490,10 +725,10 @@ private long publishBatch(final Map staged) {
* @param stagedPartition private SPO staging for this graph
* @return number of new statements in this graph
*/
- private static long mergeStagedPartition(final State target, final State replacement, final Resource graph,
+ private static long mergeStagedPartition(final PartitionState target, final PartitionState replacement, final Resource graph,
final StagedPartition stagedPartition) {
final TripleIndex additions = stagedPartition.statements;
- final Partition existing = target.graphs.get(graph);
+ final GraphPartition existing = target.graphs.get(graph);
final TripleIndex merged = existing == null ? additions : existing.spo.copy();
final long before = merged.size();
if (existing != null) {
@@ -1504,7 +739,7 @@ private static long mergeStagedPartition(final State target, final State replace
}
final long count = existing == null ? merged.size() : merged.size() - before;
if (count != 0) {
- final Partition built = Partition.build(merged, existing == null ? graph : existing.context,
+ final GraphPartition built = GraphPartition.build(merged, existing == null ? graph : existing.context,
existing == null ? replacement.nextOrdinal++ : existing.ordinal);
if (existing != null) {
existing.references--;
@@ -1636,7 +871,7 @@ public PredicateStatistics getPredicateStatistics(IRI predicate) {
long distinctObjects = 0;
boolean found = false;
- for (Partition p : state().graphs.values()) {
+ for (GraphPartition p : state().graphs.values()) {
PredicateStatistics ps = p.pos.estimatePredicateStatistics(predicate, 512);
if (ps != null) {
found = true;
@@ -1704,181 +939,9 @@ public Transaction beginTransaction(final IsolationLevel level) {
if (transaction != null) {
throw new IllegalStateException("A transaction is already active on this session");
}
- transaction = new SnapshotTransaction(database.committed.copy(), database.version);
+ transaction = new SnapshotTransaction(database, this, database.committed.copy(), database.version);
return transaction;
}
}
- /**
- * Thread-owned optimistic transaction retaining a captured state until commit or rollback.
- */
- private final class SnapshotTransaction implements Transaction {
- /**
- * Stable transaction identifier.
- */
- private final String id = UUID.randomUUID().toString();
- /**
- * Owning manager flag or transaction thread, according to the enclosing class.
- */
- private final Thread owner = Thread.currentThread();
- /**
- * Committed version captured for optimistic conflict detection.
- */
- private final long version;
- /** Version of the transaction's own visible content, independent of later commits. */
- private long revision;
- /** Independent query snapshots never publish mutations. */
- private final boolean readOnly;
- /**
- * Transaction-owned state until publication or release.
- */
- private State state;
- /**
- * Whether this transaction changed its captured state.
- */
- private boolean dirty;
- /**
- * Current transaction lifecycle status.
- */
- private TransactionState status = TransactionState.ACTIVE;
-
- /**
- * Creates a thread-owned transaction for the captured committed version.
- *
- * @param state state whose ownership is guarded by the database monitor
- * @param version committed version captured for conflict detection
- */
- private SnapshotTransaction(final State state, final long version) {
- this(state, version, false);
- }
-
- private SnapshotTransaction(final State state, final long version, final boolean readOnly) {
- this.state = state;
- this.version = version;
- this.revision = version;
- this.readOnly = readOnly;
- }
-
- /**
- * Requires access from the thread that began the transaction.
- *
- * @throws IllegalStateException if the required representation, ownership or lifecycle state is not satisfied
- */
- private void checkOwner() {
- if (Thread.currentThread() != owner) {
- throw new IllegalStateException("Transaction belongs to another thread");
- }
- }
-
- /**
- * Requires the transaction owner, active status and no overlapping session batch.
- *
- * @throws IllegalStateException if the required representation, ownership or lifecycle state is not satisfied
- */
- private void requireActive() {
- checkOwner();
- if (status != TransactionState.ACTIVE) {
- throw new IllegalStateException("Transaction is inactive");
- }
- if (loading) {
- throw new IllegalStateException("A batch is active on this session");
- }
- }
-
- /**
- * {@inheritDoc}
- *
- * @return the transaction identifier
- */
- @Override
- public String getId() {
- return id;
- }
-
- /**
- * {@inheritDoc}
- *
- * @return true while the transaction is active
- */
- @Override
- public boolean isActive() {
- synchronized (database) {
- return status == TransactionState.ACTIVE;
- }
- }
-
- /**
- * {@inheritDoc}
- *
- * @return the current lifecycle or transaction status
- */
- @Override
- public TransactionState getState() {
- synchronized (database) {
- return status;
- }
- }
-
- /**
- * {@inheritDoc}
- *
- * @throws StorageException if publication fails or a concurrent committed version prevents commit
- */
- @Override
- public void commit() {
- synchronized (database) {
- requireActive();
- checkAccess();
- if (database.version != version) {
- throw new StorageException(ErrorCode.TRANSACTION_ERROR,
- "Concurrent storage modification; roll back and retry");
- }
- if (dirty) {
- database.committed.release();
- database.committed = state;
- state = null; // Ownership transferred to the committed state.
- database.version = revision;
- }
- finish(TransactionState.COMMITTED);
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void rollback() {
- synchronized (database) {
- requireActive();
- finish(TransactionState.ROLLED_BACK);
- }
- }
-
- /**
- * Sets terminal transaction status and releases any state ownership not transferred at commit.
- *
- * @param terminal terminal transaction status
- */
- private void finish(final TransactionState terminal) {
- status = terminal;
- if (state != null) {
- state.release();
- }
- state = null;
- transaction = null;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void close() {
- synchronized (database) {
- if (isActive()) {
- rollback();
- }
- }
- }
- }
-
}
diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/MemoryDatabase.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/MemoryDatabase.java
new file mode 100644
index 000000000..5ccbbb697
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/MemoryDatabase.java
@@ -0,0 +1,93 @@
+package fr.inria.corese.core.next.storage.impl.memory;
+
+import fr.inria.corese.core.next.storage.api.config.StorageConfig;
+import fr.inria.corese.core.next.storage.api.lifecycle.LifecycleState;
+import fr.inria.corese.core.next.storage.api.lifecycle.StorageLifecycle;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Shared database state and lifecycle monitor for all in-memory sessions.
+ */
+final class MemoryDatabase implements StorageLifecycle {
+
+ /**
+ * Committed state whose partition references are owned by the database.
+ */
+ PartitionState committed = new PartitionState();
+
+ /**
+ * Committed version captured for optimistic conflict detection.
+ */
+ long version;
+
+ /**
+ * Current shared database lifecycle status.
+ */
+ LifecycleState lifecycleState = LifecycleState.NOT_INITIALIZED;
+
+ /**
+ * Configuration available during the running lifecycle.
+ */
+ StorageConfig config;
+
+ /**
+ * Creates an uninitialized database and its exclusively owned empty committed state.
+ */
+ MemoryDatabase() {
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param config storage configuration selecting this backend
+ * @throws IllegalStateException if already initialized
+ * @throws NullPointerException if {@code config} is null
+ */
+ @Override
+ public synchronized void initialize(final StorageConfig config) {
+ Objects.requireNonNull(config, "config");
+ if (lifecycleState != LifecycleState.NOT_INITIALIZED) {
+ throw new IllegalStateException("Already initialized");
+ }
+ this.config = config;
+ lifecycleState = LifecycleState.RUNNING;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @throws IllegalStateException if storage is not running
+ */
+ @Override
+ public synchronized void shutdown() {
+ if (lifecycleState != LifecycleState.RUNNING) {
+ throw new IllegalStateException("Storage is not running");
+ }
+ committed.release();
+ committed = new PartitionState();
+ config = null;
+ lifecycleState = LifecycleState.SHUTDOWN;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return current lifecycle state
+ */
+ @Override
+ public synchronized LifecycleState getState() {
+ return lifecycleState;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return current configuration when the database is running
+ */
+ @Override
+ public synchronized Optional getConfig() {
+ return Optional.ofNullable(config);
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/PartitionState.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/PartitionState.java
new file mode 100644
index 000000000..120c972bd
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/PartitionState.java
@@ -0,0 +1,119 @@
+package fr.inria.corese.core.next.storage.impl.memory;
+
+import fr.inria.corese.core.next.data.api.term.Resource;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Ordered graph registry whose partition references are acquired and released together.
+ */
+final class PartitionState {
+
+ /**
+ * Ordered partition registry; empty named graphs are retained until DROP.
+ * A null key represents the default partition.
+ */
+ final Map graphs = new LinkedHashMap<>();
+
+ /**
+ * Number of live entries or statements owned by this container.
+ */
+ long size;
+
+ /**
+ * Ordinal assigned to the next created graph.
+ */
+ long nextOrdinal;
+
+ /**
+ * Creates an empty ordered registry with exclusive ownership of its map.
+ */
+ PartitionState() {
+ }
+
+ /**
+ * Copies owned mutable containers while sharing immutable keys and statements.
+ *
+ * @return an independently owned copy of the mutable containers
+ */
+ PartitionState copy() {
+ final PartitionState copy = new PartitionState();
+ graphs.forEach((graph, partition) -> {
+ partition.references++;
+ copy.graphs.put(graph, partition);
+ });
+ copy.size = size;
+ copy.nextOrdinal = nextOrdinal;
+ return copy;
+ }
+
+ /**
+ * Capture selected graphs only; writes to unrelated graphs need no cursor-induced COW.
+ *
+ * @param contexts selected graph identifiers; an empty array selects all graphs
+ * @return an independently owned copy of the mutable containers
+ */
+ PartitionState copy(final Resource[] contexts) {
+ if (contexts.length == 0) {
+ return copy();
+ }
+ final PartitionState copy = new PartitionState();
+ final List selected = new ArrayList<>();
+ for (final Resource graph : contexts) {
+ final GraphPartition partition = graphs.get(graph);
+ if (partition != null) {
+ selected.add(partition);
+ }
+ }
+ selected.sort(Comparator.comparingLong(partition -> partition.ordinal));
+ for (final GraphPartition partition : selected) {
+ partition.references++;
+ copy.graphs.put(partition.context, partition);
+ copy.size += partition.spo.size();
+ }
+ copy.nextOrdinal = nextOrdinal;
+ return copy;
+ }
+
+ /**
+ * Detaches only a shared partition, preserving existing readers and graph order.
+ *
+ * @param graph named graph identifier or staging registry key, according to the operation
+ * @return the exclusively owned partition, or null if the graph is absent
+ */
+ GraphPartition writable(final Resource graph) {
+ final GraphPartition partition = graphs.get(graph);
+ if (partition != null && partition.references > 1) {
+ final GraphPartition replacement = partition.copy();
+ partition.references--;
+ graphs.put(graph, replacement);
+ return replacement;
+ }
+ return partition;
+ }
+
+ /**
+ * Replaces a partition with an empty version, preserving graph identity and registry position.
+ *
+ * @param partition partition retained or exclusively owned by the supplied state
+ */
+ void empty(final GraphPartition partition) {
+ // Keep the graph's identity and registry position. Readers continue to own
+ // the old partition; clearing never needs to clone its three indexes.
+ final GraphPartition replacement = new GraphPartition(partition.context, partition.ordinal);
+ graphs.put(partition.context, replacement);
+ partition.references--;
+ }
+
+ /**
+ * Releases this state's ownership; clearing the registry makes repeated release harmless.
+ */
+ void release() {
+ graphs.values().forEach(partition -> partition.references--);
+ graphs.clear();
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/PredicateStatisticsEstimator.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/PredicateStatisticsEstimator.java
new file mode 100644
index 000000000..9c41ee32b
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/PredicateStatisticsEstimator.java
@@ -0,0 +1,95 @@
+package fr.inria.corese.core.next.storage.impl.memory;
+
+import fr.inria.corese.core.next.data.api.model.Statement;
+import fr.inria.corese.core.next.data.api.term.IRI;
+import fr.inria.corese.core.next.storage.api.model.PredicateStatistics;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Estimates predicate cardinality and distinct term distribution from POS index branches.
+ */
+final class PredicateStatisticsEstimator {
+
+ private PredicateStatisticsEstimator() {
+ // Utility class
+ }
+
+ /**
+ * Estimates predicate statistics from the POS permutation.
+ *
+ * @param index POS triple index
+ * @param predicate the predicate IRI
+ * @param maxSample maximum number of branch entries to sample for distinct subjects
+ * @return predicate statistics or null if not found or not in POS order
+ */
+ static PredicateStatistics estimate(final TripleIndex index, final IRI predicate, final int maxSample) {
+ if (index.order != TripleIndex.Order.POS || predicate == null) {
+ return null;
+ }
+ final TermKey pKey = new TermKey(predicate);
+ final TripleIndex.Branch branch = index.roots.get(pKey);
+ if (branch == null) {
+ return null;
+ }
+ final long distinctObjects = branch.size();
+ if (distinctObjects == 0) {
+ return new PredicateStatistics(predicate, 0, 0, 0);
+ }
+
+ final PredicateSampleAccumulator accumulator = new PredicateSampleAccumulator(maxSample);
+ branch.forEach((objKey, entry) -> accumulator.accumulate(entry));
+
+ final long distinctSubjects = accumulator.estimateDistinctSubjects(distinctObjects);
+ return new PredicateStatistics(predicate, accumulator.totalCount, distinctSubjects, distinctObjects);
+ }
+
+ private static final class PredicateSampleAccumulator {
+ private final Set subjects = new HashSet<>();
+ private final int maxSample;
+ private long totalCount = 0;
+ private int sampled = 0;
+
+ PredicateSampleAccumulator(final int maxSample) {
+ this.maxSample = maxSample;
+ }
+
+ void accumulate(final Object entry) {
+ if (entry instanceof TripleIndex.Leaf leaf) {
+ accumulateLeaf(leaf);
+ } else if (entry instanceof Statement statement) {
+ accumulateStatement(statement);
+ }
+ }
+
+ private void accumulateLeaf(final TripleIndex.Leaf leaf) {
+ totalCount += leaf.size();
+ if (shouldSample()) {
+ leaf.forEach((subjKey, st) -> subjects.add(subjKey));
+ sampled++;
+ }
+ }
+
+ private void accumulateStatement(final Statement statement) {
+ totalCount++;
+ if (shouldSample()) {
+ subjects.add(new TermKey(statement.getSubject()));
+ sampled++;
+ }
+ }
+
+ private boolean shouldSample() {
+ return maxSample <= 0 || sampled < maxSample;
+ }
+
+ long estimateDistinctSubjects(final long distinctObjects) {
+ if (maxSample <= 0 || distinctObjects <= maxSample) {
+ return subjects.size();
+ }
+ final double ratio = (double) distinctObjects / Math.max(1, sampled);
+ final long estimated = Math.round(subjects.size() * ratio);
+ return Math.min(totalCount, Math.max(subjects.size(), estimated));
+ }
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/SecondaryIndexBuilder.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/SecondaryIndexBuilder.java
new file mode 100644
index 000000000..744b559ac
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/SecondaryIndexBuilder.java
@@ -0,0 +1,36 @@
+package fr.inria.corese.core.next.storage.impl.memory;
+
+/**
+ * Builds secondary POS and OSP index permutations from a staged SPO triple index.
+ */
+final class SecondaryIndexBuilder {
+
+ private SecondaryIndexBuilder() {
+ // Utility class
+ }
+
+ /**
+ * Completed secondary permutations sharing the SPO statements and canonical keys.
+ *
+ * @param pos completed POS permutation
+ * @param osp completed OSP permutation
+ */
+ record Result(TripleIndex pos, TripleIndex osp) {
+ }
+
+ /**
+ * Reconstruct omitted singleton keys once while building both secondary permutations.
+ *
+ * @param source source index or container; it must remain unchanged during copying or traversal
+ * @return the completed POS and OSP permutations
+ */
+ static Result build(final TripleIndex source) {
+ final TripleIndex pos = new TripleIndex(TripleIndex.Order.POS);
+ final TripleIndex osp = new TripleIndex(TripleIndex.Order.OSP);
+ source.visitEntries((subject, predicate, object, statement) -> {
+ pos.add(statement, subject, predicate, object, source);
+ osp.add(statement, subject, predicate, object, source);
+ });
+ return new Result(pos, osp);
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/SnapshotStatementSpliterator.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/SnapshotStatementSpliterator.java
new file mode 100644
index 000000000..a230d1e0c
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/SnapshotStatementSpliterator.java
@@ -0,0 +1,153 @@
+package fr.inria.corese.core.next.storage.impl.memory;
+
+import fr.inria.corese.core.next.data.api.model.Statement;
+import fr.inria.corese.core.next.storage.api.model.StatementPattern;
+import fr.inria.corese.core.next.storage.api.operations.StatementSpliterator;
+
+import java.util.Iterator;
+import java.util.Objects;
+import java.util.Spliterator;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Single-consumer cursor owning a captured read state until close, exhaustion or failure.
+ *
+ *
+ * Known-size traversal can release ownership before delivering the last immutable statement. Close is idempotent
+ * and clears traversal references under the monitor. No database monitor is held while visiting rows or invoking
+ * consumer code. Session and transaction closure do not invalidate this independent read snapshot.
+ *
+ */
+final class SnapshotStatementSpliterator implements StatementSpliterator, AutoCloseable {
+
+ private final Object monitor;
+ private PartitionState snapshot;
+ private Iterator partitions;
+ private TripleIndexCursor rows;
+ private final StatementPattern pattern;
+ private final TermKey subject;
+ private final TermKey predicate;
+ private final TermKey object;
+ private final boolean sized;
+ private final boolean distinctTriples;
+ private long remaining;
+
+ SnapshotStatementSpliterator(final Object monitor, final PartitionState snapshot, final StatementPattern pattern) {
+ this.monitor = Objects.requireNonNull(monitor, "monitor");
+ this.snapshot = Objects.requireNonNull(snapshot, "snapshot");
+ this.pattern = Objects.requireNonNull(pattern, "pattern");
+ this.distinctTriples = hasAtMostOnePopulatedPartition(snapshot);
+ this.subject = TermKey.nullable(pattern.getSubject());
+ this.predicate = TermKey.nullable(pattern.getPredicate());
+ this.object = TermKey.nullable(pattern.getObject());
+ this.sized = subject == null && predicate == null && object == null;
+ this.remaining = sized ? snapshot.size : Long.MAX_VALUE;
+ this.partitions = snapshot.graphs.values().iterator();
+ }
+
+ private static boolean hasAtMostOnePopulatedPartition(final PartitionState snapshot) {
+ if (snapshot.graphs.size() <= 1) {
+ return true;
+ }
+ boolean populated = false;
+ for (final GraphPartition partition : snapshot.graphs.values()) {
+ if (partition.spo.size() == 0) {
+ continue;
+ }
+ if (populated) {
+ return false;
+ }
+ populated = true;
+ }
+ return true;
+ }
+
+ @SuppressWarnings("java:S1181")
+ @Override
+ public boolean tryAdvance(final Consumer super Statement> action) {
+ Objects.requireNonNull(action, "action");
+ if (snapshot == null) {
+ return false;
+ }
+ try {
+ for (;;) {
+ final Statement statement = rows == null ? null : rows.next();
+ if (statement != null) {
+ if (sized && --remaining == 0) {
+ close();
+ }
+ action.accept(statement);
+ return true;
+ }
+ if (!partitions.hasNext()) {
+ close();
+ return false;
+ }
+ rows = partitions.next().index(pattern).cursor(subject, predicate, object);
+ }
+ } catch (final RuntimeException | Error failure) {
+ close();
+ throw failure;
+ }
+ }
+
+ @Override
+ public void forEachRemaining(final Consumer super Statement> action) {
+ Objects.requireNonNull(action, "action");
+ if (snapshot == null) {
+ return;
+ }
+ if (rows != null) {
+ StatementSpliterator.super.forEachRemaining(action);
+ return;
+ }
+ try {
+ final Predicate visitor = statement -> {
+ if (sized && --remaining == 0) {
+ close();
+ }
+ action.accept(statement);
+ return snapshot == null;
+ };
+ while (snapshot != null && partitions.hasNext()) {
+ partitions.next().index(pattern).visit(subject, predicate, object, visitor);
+ }
+ } finally {
+ close();
+ }
+ }
+
+ @Override
+ public boolean hasDistinctTriples() {
+ return distinctTriples;
+ }
+
+ @Override
+ public Spliterator trySplit() {
+ return null;
+ }
+
+ @Override
+ public long estimateSize() {
+ return snapshot == null ? 0 : remaining;
+ }
+
+ @Override
+ public int characteristics() {
+ return ORDERED | NONNULL | IMMUTABLE | (sized ? SIZED | SUBSIZED : 0);
+ }
+
+ @Override
+ public void close() {
+ synchronized (monitor) {
+ if (snapshot == null) {
+ return;
+ }
+ snapshot.release();
+ snapshot = null;
+ partitions = null;
+ rows = null;
+ }
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/SnapshotTransaction.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/SnapshotTransaction.java
new file mode 100644
index 000000000..cfb709e80
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/SnapshotTransaction.java
@@ -0,0 +1,121 @@
+package fr.inria.corese.core.next.storage.impl.memory;
+
+import fr.inria.corese.core.next.storage.api.exception.ErrorCode;
+import fr.inria.corese.core.next.storage.api.exception.StorageException;
+import fr.inria.corese.core.next.storage.api.transaction.Transaction;
+import fr.inria.corese.core.next.storage.api.transaction.TransactionState;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Thread-owned optimistic transaction retaining a captured state until commit or rollback.
+ */
+final class SnapshotTransaction implements Transaction {
+
+ private final String id = UUID.randomUUID().toString();
+ private final Thread owner = Thread.currentThread();
+ private final MemoryDatabase database;
+ private final IndexedStatementStore session;
+ private final long version;
+ long revision;
+ final boolean readOnly;
+ PartitionState state;
+ boolean dirty;
+ private TransactionState status = TransactionState.ACTIVE;
+
+ SnapshotTransaction(final MemoryDatabase database, final IndexedStatementStore session, final PartitionState state,
+ final long version) {
+ this(database, session, state, version, false);
+ }
+
+ SnapshotTransaction(final MemoryDatabase database, final IndexedStatementStore session, final PartitionState state,
+ final long version, final boolean readOnly) {
+ this.database = Objects.requireNonNull(database, "database");
+ this.session = Objects.requireNonNull(session, "session");
+ this.state = Objects.requireNonNull(state, "state");
+ this.version = version;
+ this.revision = version;
+ this.readOnly = readOnly;
+ }
+
+ void checkOwner() {
+ if (Thread.currentThread() != owner) {
+ throw new IllegalStateException("Transaction belongs to another thread");
+ }
+ }
+
+ private void requireActive() {
+ checkOwner();
+ if (status != TransactionState.ACTIVE) {
+ throw new IllegalStateException("Transaction is inactive");
+ }
+ if (session.isLoading()) {
+ throw new IllegalStateException("A batch is active on this session");
+ }
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public boolean isActive() {
+ synchronized (database) {
+ return status == TransactionState.ACTIVE;
+ }
+ }
+
+ @Override
+ public TransactionState getState() {
+ synchronized (database) {
+ return status;
+ }
+ }
+
+ @Override
+ public void commit() {
+ synchronized (database) {
+ requireActive();
+ session.checkAccess();
+ if (database.version != version) {
+ throw new StorageException(ErrorCode.TRANSACTION_ERROR,
+ "Concurrent storage modification; roll back and retry");
+ }
+ if (dirty) {
+ database.committed.release();
+ database.committed = state;
+ state = null; // Ownership transferred to the committed state.
+ database.version = revision;
+ }
+ finish(TransactionState.COMMITTED);
+ }
+ }
+
+ @Override
+ public void rollback() {
+ synchronized (database) {
+ requireActive();
+ finish(TransactionState.ROLLED_BACK);
+ }
+ }
+
+ void finish(final TransactionState terminal) {
+ status = terminal;
+ if (state != null) {
+ state.release();
+ }
+ state = null;
+ session.clearTransaction();
+ }
+
+ @Override
+ public void close() {
+ synchronized (database) {
+ if (isActive()) {
+ rollback();
+ }
+ }
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/StagingTerms.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/StagingTerms.java
index cb5ee44ff..a8aacc98d 100644
--- a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/StagingTerms.java
+++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/StagingTerms.java
@@ -41,15 +41,16 @@ final class StagingTerms extends HashMap {
}
/**
- * Captures matching graph partitions immediately and returns a sequential lazy stream that must be closed.
+ * Looks up an existing canonical term key matching the given value and structural hash.
*
*
* The private, non-stored probe is passed to HashMap.get(Object), which compares the search object's
* equals(storedKey). Stored keys always remain immutable TermKey instances.
+ *
*
- * @param value non-null RDF value or entry value
- * @param hash cached structural hash of the captured RDF value
- * @return a sequential stream owning its read snapshot; the caller must close it
+ * @param value non-null RDF value to look up
+ * @param hash cached structural hash of the RDF value
+ * @return the matching canonical TermKey, or null if not found
*/
@SuppressWarnings("java:S2175")
TermKey find(final Value value, final int hash) {
diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/StatementCapture.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/StatementCapture.java
new file mode 100644
index 000000000..17ddc4b1a
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/StatementCapture.java
@@ -0,0 +1,190 @@
+package fr.inria.corese.core.next.storage.impl.memory;
+
+import fr.inria.corese.core.next.data.Values;
+import fr.inria.corese.core.next.data.api.model.SimpleStatement;
+import fr.inria.corese.core.next.data.api.model.Statement;
+import fr.inria.corese.core.next.data.api.term.BNode;
+import fr.inria.corese.core.next.data.api.term.IRI;
+import fr.inria.corese.core.next.data.api.term.Literal;
+import fr.inria.corese.core.next.data.api.term.Resource;
+import fr.inria.corese.core.next.data.api.term.SimpleBNode;
+import fr.inria.corese.core.next.data.api.term.SimpleDate;
+import fr.inria.corese.core.next.data.api.term.SimpleDateTime;
+import fr.inria.corese.core.next.data.api.term.SimpleDecimal;
+import fr.inria.corese.core.next.data.api.term.SimpleDouble;
+import fr.inria.corese.core.next.data.api.term.SimpleDuration;
+import fr.inria.corese.core.next.data.api.term.SimpleIRI;
+import fr.inria.corese.core.next.data.api.term.SimpleInteger;
+import fr.inria.corese.core.next.data.api.term.SimpleLiteral;
+import fr.inria.corese.core.next.data.api.term.SimpleTime;
+import fr.inria.corese.core.next.data.api.term.SimpleTriple;
+import fr.inria.corese.core.next.data.api.term.Triple;
+import fr.inria.corese.core.next.data.api.term.Value;
+import fr.inria.corese.core.next.storage.api.model.StatementPattern;
+
+import java.util.Objects;
+
+/**
+ * Defensive RDF term and statement capture utilities for in-memory graph storage.
+ *
+ *
+ * Ensures that incoming RDF terms, statements, and search patterns are converted to immutable, safe internal
+ * representations, and enables term sharing across statements without altering RDF semantics.
+ *
+ */
+final class StatementCapture {
+
+ private StatementCapture() {
+ // Utility class
+ }
+
+ /**
+ * Converts an arbitrary RDF value into an immutable, thread-safe internal representation.
+ *
+ * @param value non-null RDF value to capture
+ * @return the captured immutable RDF value
+ * @throws IllegalArgumentException if the RDF term implementation is unsupported
+ * @throws NullPointerException if {@code value} is null
+ */
+ static Value immutable(final Value value) {
+ Objects.requireNonNull(value, "value");
+ if (value instanceof Literal literal && !(literal.getDatatype() instanceof SimpleIRI)) {
+ final IRI datatype = (IRI) immutable(literal.getDatatype());
+ if (value instanceof SimpleLiteral) {
+ return literal.getLanguage().map(language -> (Value) new SimpleLiteral(literal.getLabel(), language))
+ .orElseGet(() -> new SimpleLiteral(literal.getLabel(), datatype));
+ }
+ return Values.factory().createLiteral(literal.getLabel(), datatype);
+ }
+ if (value instanceof SimpleIRI || value instanceof SimpleBNode || value instanceof SimpleLiteral
+ || value instanceof SimpleInteger || value instanceof SimpleDecimal || value instanceof SimpleDouble
+ || value instanceof SimpleDate || value instanceof SimpleDateTime || value instanceof SimpleTime
+ || value instanceof SimpleDuration) {
+ return value;
+ }
+ if (value instanceof IRI iri) {
+ return new SimpleIRI(iri.stringValue());
+ }
+ if (value instanceof BNode blank) {
+ return new SimpleBNode(blank.getID());
+ }
+ if (value instanceof Literal literal) {
+ return literal.getLanguage().map(language -> (Value) new SimpleLiteral(literal.getLabel(), language))
+ .orElseGet(() -> new SimpleLiteral(literal.getLabel(), (IRI) immutable(literal.getDatatype())));
+ }
+ if (value instanceof Triple triple) {
+ return new SimpleTriple((Resource) immutable(triple.subject()), (IRI) immutable(triple.predicate()),
+ immutable(triple.object()));
+ }
+ throw new IllegalArgumentException("Unsupported RDF term: " + value.getClass().getName());
+ }
+
+ /**
+ * Converts an arbitrary statement into an immutable representation with captured components.
+ *
+ * @param statement non-null statement to capture
+ * @return an immutable captured statement
+ * @throws NullPointerException if {@code statement} is null
+ */
+ static Statement immutable(final Statement statement) {
+ Objects.requireNonNull(statement, "statement");
+ final Resource context = statement.getContext();
+ final Resource subject = (Resource) immutable(statement.getSubject());
+ final IRI predicate = (IRI) immutable(statement.getPredicate());
+ final Value object = immutable(statement.getObject());
+ final Resource graph = context == null ? null : (Resource) immutable(context);
+ if (statement.getClass() == SimpleStatement.class && subject == statement.getSubject()
+ && predicate == statement.getPredicate() && object == statement.getObject() && graph == context) {
+ return statement;
+ }
+ return new SimpleStatement(subject, predicate, object, graph);
+ }
+
+ /**
+ * Converts a statement pattern into an immutable representation with captured components.
+ *
+ * @param pattern non-null search pattern to capture
+ * @return an immutable statement pattern
+ * @throws NullPointerException if {@code pattern} is null
+ */
+ static StatementPattern immutable(final StatementPattern pattern) {
+ Objects.requireNonNull(pattern, "pattern");
+ final Resource[] contexts = pattern.getContexts();
+ for (int i = 0; i < contexts.length; i++) {
+ if (contexts[i] != null) {
+ contexts[i] = (Resource) immutable(contexts[i]);
+ }
+ }
+ return StatementPattern.of(pattern.getSubject() == null ? null : (Resource) immutable(pattern.getSubject()),
+ pattern.getPredicate() == null ? null : (IRI) immutable(pattern.getPredicate()),
+ pattern.getObject() == null ? null : immutable(pattern.getObject()), contexts);
+ }
+
+ /**
+ * Reuses compatible canonical RDF values without changing lexical identity or implementation type.
+ *
+ * @param statement statement whose RDF identities have been captured
+ * @param subject canonical subject key
+ * @param predicate canonical predicate key
+ * @param object canonical object key
+ * @param context canonical graph identifier; null denotes the default graph
+ * @return the original statement when all values are already shared, otherwise a statement using compatible
+ * canonical values
+ */
+ static Statement sharedStatement(final Statement statement, final TermKey subject, final TermKey predicate,
+ final TermKey object, final Resource context) {
+ final Resource s = (Resource) sharedValue(statement.getSubject(), subject.value());
+ final IRI p = (IRI) sharedValue(statement.getPredicate(), predicate.value());
+ final Value o = sharedValue(statement.getObject(), object.value());
+ final Resource g = context == null ? null : (Resource) sharedValue(statement.getContext(), context);
+ return s == statement.getSubject() && p == statement.getPredicate() && o == statement.getObject()
+ && g == statement.getContext() ? statement : new SimpleStatement(s, p, o, g);
+ }
+
+ /**
+ * Reuses a compatible immutable scalar value while preserving its implementation and datatype.
+ *
+ * @param original original captured RDF value
+ * @param canonical compatible canonical RDF value
+ * @return the compatible canonical value, or the original value when pooling is unsuitable
+ */
+ private static Value sharedValue(final Value original, final Value canonical) {
+ if (original == canonical || original.getClass() != canonical.getClass()) {
+ return original;
+ }
+ if (original instanceof Literal literal && canonical instanceof Literal other
+ && !sameLiteralImplementation(literal, other)) {
+ return original;
+ }
+ if (original instanceof SimpleIRI iri && canonical instanceof SimpleIRI other) {
+ return iri.getNamespace().equals(other.getNamespace()) && iri.getLocalName().equals(other.getLocalName())
+ ? canonical
+ : original;
+ }
+ if (original instanceof SimpleLiteral literal && canonical instanceof SimpleLiteral other) {
+ return literal.getCoreDatatype() == other.getCoreDatatype() ? canonical : original;
+ }
+ // Calendar-backed terms and nested triples are deliberately not pooled.
+ return original instanceof SimpleBNode || original instanceof SimpleInteger || original instanceof SimpleDecimal
+ || original instanceof SimpleDouble || original instanceof SimpleDuration ? canonical : original;
+ }
+
+ /**
+ * Checks literal datatype implementation compatibility using the existing comparison order.
+ *
+ * @param literal original captured literal
+ * @param other value or search object to compare with the stored key
+ * @return true when datatype implementation and native components permit sharing
+ */
+ private static boolean sameLiteralImplementation(final Literal literal, final Literal other) {
+ if (literal.getCoreDatatype() != other.getCoreDatatype()) {
+ return false;
+ }
+ final IRI datatype = literal.getDatatype();
+ final IRI canonicalDatatype = other.getDatatype();
+
+ return datatype == canonicalDatatype || (datatype.getClass() == canonicalDatatype.getClass()
+ && datatype.getNamespace().equals(canonicalDatatype.getNamespace())
+ && datatype.getLocalName().equals(canonicalDatatype.getLocalName()));
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/TripleIndex.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/TripleIndex.java
index 9e099a6b4..e68c82f86 100644
--- a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/TripleIndex.java
+++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/TripleIndex.java
@@ -58,7 +58,7 @@ enum Order {
* @param s captured statement to index
* @return the third permutation component, or null for a wildcard
*/
- private Value third(final Statement s) {
+ Value third(final Statement s) {
return switch (this) {
case SPO -> s.getObject();
case POS -> s.getSubject();
@@ -112,11 +112,11 @@ private Value third(final StatementPattern p) {
/**
* Component order of this triple permutation.
*/
- private final Order order;
+ final Order order;
/**
* Insertion-ordered first-component branches.
*/
- private final Map roots;
+ final Map roots;
// Bulk-only canonicalization cache, discarded before publishing a partition.
/**
* Batch-private canonical dictionary, cleared before publication.
@@ -197,7 +197,7 @@ boolean add(final Statement s, final Resource context) {
final TermKey subject = stagedKey(s.getSubject());
final TermKey predicate = stagedKey(s.getPredicate());
final TermKey object = stagedKey(s.getObject());
- return add(IndexedStatementStore.sharedStatement(s, subject, predicate, object, context), subject, predicate,
+ return add(StatementCapture.sharedStatement(s, subject, predicate, object, context), subject, predicate,
object);
}
@@ -608,184 +608,11 @@ TripleIndex copy() {
* @param object canonical object key, or null for a wildcard search
* @return a single-consumer positional cursor
*/
- Cursor cursor(final TermKey subject, final TermKey predicate, final TermKey object) {
- return new Cursor(first(subject, predicate, object), second(subject, predicate, object),
+ TripleIndexCursor cursor(final TermKey subject, final TermKey predicate, final TermKey object) {
+ return new TripleIndexCursor(this, first(subject, predicate, object), second(subject, predicate, object),
third(subject, predicate, object));
}
- /**
- * Single-consumer positional cursor over an unchanged partition retained by its caller.
- */
- final class Cursor {
- /**
- * Captured first-component search key; null denotes a wildcard.
- */
- private final TermKey first;
- /**
- * Captured second-component search key; null denotes a wildcard.
- */
- private final TermKey second;
- /**
- * Captured third-component search key; null denotes a wildcard.
- */
- private final TermKey third;
- /**
- * Next root offset to inspect in the retained table.
- */
- private int rootPosition;
- /**
- * Next branch offset to inspect.
- */
- private int branchPosition;
- /**
- * Next multivalued leaf offset to inspect.
- */
- private int leafPosition;
- /**
- * Whether the explicitly selected root has already been visited.
- */
- private boolean selectedRoot;
- /**
- * Current unchanged branch retained by the enclosing snapshot.
- */
- private Branch branch;
- /**
- * Current unchanged multivalued leaf.
- */
- private Leaf leaf;
-
- /**
- * Captures the three search keys for unchanged positional traversal.
- *
- * @param first first-component key; null selects all roots
- * @param second second-component key; null selects all branch values
- * @param third third-component key; null selects all leaf values
- */
- private Cursor(final TermKey first, final TermKey second, final TermKey third) {
- this.first = first;
- this.second = second;
- this.third = third;
- }
-
- /**
- * Internal cursor: null marks exhaustion. The public Spliterator supplies Iterator semantics.
- *
- * @return the next statement, or null at exhaustion for the internal index cursor
- */
- Statement next() {
- for (;;) {
- if (leaf != null) {
- final Statement result = nextLeafStatement();
- if (result != null) {
- return result;
- }
- }
- if (branch != null) {
- final Statement result = nextBranchStatement();
- if (result != null) {
- return result;
- }
- continue;
- }
- if (!selectNextRoot()) {
- return null;
- }
- branchPosition = 0;
- }
- }
-
- /**
- * Reads the next match from the current leaf and drops that traversal reference on exhaustion.
- *
- * @return the next matching leaf statement, or null when the leaf is exhausted
- */
- private Statement nextLeafStatement() {
- if (third != null) {
- final Statement result = leaf.get(third);
- leaf = null;
- if (result != null) {
- return result;
- }
- } else {
- final int position = leaf.nextIndex(leafPosition);
- if (position >= 0) {
- leafPosition = position + 1;
- return leaf.valueAt(position);
- }
- leaf = null;
- }
- return null;
- }
-
- /**
- * Reads a branch match or selects a multivalued leaf for subsequent traversal.
- *
- * @return a matching singleton statement, or null when traversal must continue
- */
- private Statement nextBranchStatement() {
- final Object entry = nextBranchEntry();
- if (entry instanceof Leaf values) {
- leaf = values;
- leafPosition = 0;
- return null;
- }
- if (entry != null && (third == null || third.matches(order.third((Statement) entry)))) {
- return (Statement) entry;
- }
- return null;
- }
-
- /**
- * Reads a selected branch value or advances its positional traversal.
- *
- * @return the selected branch entry, or null when no entry remains
- */
- private Object nextBranchEntry() {
- Object entry;
- if (second != null) {
- entry = branch.get(second);
- branch = null;
- } else {
- final int position = branch.nextIndex(branchPosition);
- if (position < 0) {
- branch = null;
- return null;
- }
- branchPosition = position + 1;
- entry = branch.valueAt(position);
- }
- return entry;
- }
-
- /**
- * Selects the next root once, preserving its encounter order.
- *
- * @return true when a root was selected, false at exhaustion
- */
- private boolean selectNextRoot() {
- if (first != null) {
- if (selectedRoot) {
- return false;
- }
- selectedRoot = true;
- branch = roots.get(first);
- if (branch == null) {
- return false;
- }
- } else {
- var table = (DenseTermTable) roots;
- final int position = table.nextIndex(rootPosition);
- if (position < 0) {
- return false;
- }
- rootPosition = position + 1;
- branch = table.valueAt(position);
- }
- return true;
- }
-
- }
-
/**
* Counts matching prefixes without visiting every statement in a dense leaf.
*
@@ -823,72 +650,7 @@ long count(final StatementPattern pattern) {
* @return predicate statistics or null if not found or not in POS order
*/
PredicateStatistics estimatePredicateStatistics(final IRI predicate, final int maxSample) {
- if (order != Order.POS || predicate == null) {
- return null;
- }
- final TermKey pKey = new TermKey(predicate);
- final Branch branch = roots.get(pKey);
- if (branch == null) {
- return null;
- }
- final long distinctObjects = branch.size();
- if (distinctObjects == 0) {
- return new PredicateStatistics(predicate, 0, 0, 0);
- }
-
- final PredicateSampleAccumulator accumulator = new PredicateSampleAccumulator(maxSample);
- branch.forEach((objKey, entry) -> accumulator.accumulate(entry));
-
- final long distinctSubjects = accumulator.estimateDistinctSubjects(distinctObjects);
- return new PredicateStatistics(predicate, accumulator.totalCount, distinctSubjects, distinctObjects);
- }
-
- private static final class PredicateSampleAccumulator {
- private final Set subjects = new HashSet<>();
- private final int maxSample;
- private long totalCount = 0;
- private int sampled = 0;
-
- PredicateSampleAccumulator(final int maxSample) {
- this.maxSample = maxSample;
- }
-
- void accumulate(final Object entry) {
- if (entry instanceof Leaf leaf) {
- accumulateLeaf(leaf);
- } else if (entry instanceof Statement statement) {
- accumulateStatement(statement);
- }
- }
-
- private void accumulateLeaf(final Leaf leaf) {
- totalCount += leaf.size();
- if (shouldSample()) {
- leaf.forEach((subjKey, st) -> subjects.add(subjKey));
- sampled++;
- }
- }
-
- private void accumulateStatement(final Statement statement) {
- totalCount++;
- if (shouldSample()) {
- subjects.add(new TermKey(statement.getSubject()));
- sampled++;
- }
- }
-
- private boolean shouldSample() {
- return maxSample <= 0 || sampled < maxSample;
- }
-
- long estimateDistinctSubjects(final long distinctObjects) {
- if (maxSample <= 0 || distinctObjects <= maxSample) {
- return subjects.size();
- }
- final double ratio = (double) distinctObjects / Math.max(1, sampled);
- final long estimated = Math.round(subjects.size() * ratio);
- return Math.min(totalCount, Math.max(subjects.size(), estimated));
- }
+ return PredicateStatisticsEstimator.estimate(this, predicate, maxSample);
}
/**
@@ -938,7 +700,7 @@ private long entryCount(final Object entry, final TermKey third) {
}
/** Root keys are also addressable as values for cross-permutation canonicalization. */
- private static final class Branch extends CompactTermMap