From 1cce4760880bc18099739b026f9a443696de1aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Tue, 27 Oct 2026 10:10:02 +0100 Subject: [PATCH 1/9] refactor(storage): extract StatementCapture and decouple TripleIndex --- .../impl/memory/IndexedStatementStore.java | 201 +----------------- .../storage/impl/memory/StagingTerms.java | 9 +- .../storage/impl/memory/StatementCapture.java | 190 +++++++++++++++++ .../next/storage/impl/memory/TripleIndex.java | 2 +- 4 files changed, 206 insertions(+), 196 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/storage/impl/memory/StatementCapture.java 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..7f2d71f4b 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,24 +1,8 @@ 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; @@ -198,7 +182,7 @@ 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); + s = StatementCapture.sharedStatement(s, subject, predicate, object, context); if (!spo.add(s, subject, predicate, object, osp)) { return false; } @@ -630,171 +614,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,7 +634,7 @@ 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()); @@ -1054,7 +873,7 @@ 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()) { @@ -1117,7 +936,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); @@ -1210,7 +1029,7 @@ 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(); @@ -1242,7 +1061,7 @@ 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()); @@ -1269,7 +1088,7 @@ 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(); @@ -1358,7 +1177,7 @@ 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)) { @@ -1381,7 +1200,7 @@ 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); @@ -1437,7 +1256,7 @@ private static Map stageStatements(final Iterable 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); } 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..90183a890 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 @@ -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); } From b7775916aff9d3071d56e1afd3096bc6348b38fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Tue, 27 Oct 2026 10:18:25 +0100 Subject: [PATCH 2/9] refactor(storage): extract GraphPartition and PartitionState from IndexedStatementStore --- .../storage/impl/memory/GraphPartition.java | 208 ++++++++++ .../impl/memory/IndexedStatementStore.java | 387 +++--------------- .../storage/impl/memory/PartitionState.java | 119 ++++++ 3 files changed, 376 insertions(+), 338 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/storage/impl/memory/GraphPartition.java create mode 100644 src/main/java/fr/inria/corese/core/next/storage/impl/memory/PartitionState.java 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..718ca14b9 --- /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 TripleIndex.SecondaryIndexes secondary = TripleIndex.buildSecondaryIndexes(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 7f2d71f4b..720045284 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 @@ -46,7 +46,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 @@ -91,295 +91,6 @@ 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 = 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 - */ - 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. */ @@ -392,7 +103,7 @@ private Database() { /** * Committed state whose partition references are owned by the database. */ - private State committed = new State(); + private PartitionState committed = new PartitionState(); /** * Committed version captured for optimistic conflict detection. */ @@ -434,7 +145,7 @@ public synchronized void shutdown() { throw new IllegalStateException("Storage is not running"); } committed.release(); - committed = new State(); + committed = new PartitionState(); config = null; lifecycleState = LifecycleState.SHUTDOWN; } @@ -515,7 +226,7 @@ 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); return snapshot; @@ -584,7 +295,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; } @@ -637,7 +348,7 @@ public Stream find(StatementPattern 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); return StreamSupport.stream(cursor, false).onClose(cursor::close); @@ -658,11 +369,11 @@ private final class SnapshotCursor implements StatementSpliterator, AutoCloseabl /** * Read state retained until this cursor is closed or exhausted. */ - private State snapshot; + private PartitionState snapshot; /** * Ordered traversal of the retained partition registry. */ - private Iterator partitions; + private Iterator partitions; /** * Positional traversal of the current retained partition. */ @@ -702,7 +413,7 @@ private final class SnapshotCursor implements StatementSpliterator, AutoCloseabl * @param snapshot retained read state * @param pattern captured search pattern */ - private SnapshotCursor(final State snapshot, final StatementPattern pattern) { + private SnapshotCursor(final PartitionState snapshot, final StatementPattern pattern) { this.snapshot = snapshot; this.pattern = pattern; distinctTriples = hasAtMostOnePopulatedPartition(snapshot); @@ -847,12 +558,12 @@ public void close() { * @param snapshot retained read state * @return true when at most one selected partition contains statements */ - private static boolean hasAtMostOnePopulatedPartition(final State snapshot) { + private static boolean hasAtMostOnePopulatedPartition(final PartitionState snapshot) { if (snapshot.graphs.size() <= 1) { return true; } boolean populated = false; - for (final Partition partition : snapshot.graphs.values()) { + for (final GraphPartition partition : snapshot.graphs.values()) { if (partition.spo.size() == 0) { continue; } @@ -879,18 +590,18 @@ public long count(StatementPattern pattern) { 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); @@ -905,7 +616,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()); @@ -913,12 +624,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); } @@ -951,18 +662,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); @@ -977,20 +688,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; } @@ -1006,11 +717,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); } @@ -1032,14 +743,14 @@ public boolean add(final Statement 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); @@ -1064,11 +775,11 @@ public boolean remove(final Statement 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--; @@ -1091,17 +802,17 @@ public long remove(StatementPattern 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); } @@ -1123,7 +834,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; @@ -1145,7 +856,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); } @@ -1183,7 +894,7 @@ public boolean createGraph(final Resource graph) { 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; } @@ -1203,7 +914,7 @@ public boolean dropGraph(final Resource 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; } @@ -1272,8 +983,8 @@ private static Map stageStatements(final Iterable 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 }; @@ -1309,10 +1020,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) { @@ -1323,7 +1034,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--; @@ -1455,7 +1166,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; @@ -1551,7 +1262,7 @@ private final class SnapshotTransaction implements Transaction { /** * Transaction-owned state until publication or release. */ - private State state; + private PartitionState state; /** * Whether this transaction changed its captured state. */ @@ -1567,11 +1278,11 @@ private final class SnapshotTransaction implements Transaction { * @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) { + private SnapshotTransaction(final PartitionState state, final long version) { this(state, version, false); } - private SnapshotTransaction(final State state, final long version, final boolean readOnly) { + private SnapshotTransaction(final PartitionState state, final long version, final boolean readOnly) { this.state = state; this.version = version; this.revision = version; 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(); + } +} From 55668c8e05975a2dcc8bad5cb65e5222e610d2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Tue, 27 Oct 2026 10:50:17 +0100 Subject: [PATCH 3/9] refactor(storage): extract MemoryDatabase, SnapshotStatementSpliterator and SnapshotTransaction --- .../impl/memory/IndexedStatementStore.java | 497 +----------------- .../storage/impl/memory/MemoryDatabase.java | 93 ++++ .../memory/SnapshotStatementSpliterator.java | 153 ++++++ .../impl/memory/SnapshotTransaction.java | 121 +++++ 4 files changed, 382 insertions(+), 482 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/storage/impl/memory/MemoryDatabase.java create mode 100644 src/main/java/fr/inria/corese/core/next/storage/impl/memory/SnapshotStatementSpliterator.java create mode 100644 src/main/java/fr/inria/corese/core/next/storage/impl/memory/SnapshotTransaction.java 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 720045284..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 @@ -4,7 +4,6 @@ import fr.inria.corese.core.next.data.api.term.IRI; 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.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; @@ -15,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; @@ -25,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; @@ -77,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. */ @@ -91,91 +84,11 @@ final class IndexedStatementStore */ private boolean loading; - /** - * 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 PartitionState committed = new PartitionState(); - /** - * 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 PartitionState(); - 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()); } /** @@ -183,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; } @@ -228,7 +141,7 @@ IndexedStatementStore openReadSnapshot() { final IndexedStatementStore snapshot = new IndexedStatementStore(database); 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(); @@ -261,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"); } @@ -350,7 +271,7 @@ public Stream find(StatementPattern pattern) { checkAccess(); 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(); @@ -359,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 PartitionState 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 PartitionState 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 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 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 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; - } - /** * {@inheritDoc} * @@ -1234,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 PartitionState 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 PartitionState state, final long version) { - this(state, version, false); - } - - private SnapshotTransaction(final PartitionState 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/SnapshotStatementSpliterator.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/SnapshotStatementSpliterator.java new file mode 100644 index 000000000..f841485d8 --- /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 TripleIndex.Cursor 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 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 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(); + } + } + } +} From 1691d356815856d583ba8e509a87aea6564996ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Tue, 27 Oct 2026 11:13:43 +0100 Subject: [PATCH 4/9] refactor(storage): extract PredicateStatisticsEstimator and SecondaryIndexBuilder from TripleIndex - Extract predicate cardinality sampling logic into PredicateStatisticsEstimator - Extract POS/OSP secondary index permutation build into SecondaryIndexBuilder - Decouple GraphPartition and TripleIndex helper records - Zero SonarLint alerts and 100% tests passing --- .../storage/impl/memory/GraphPartition.java | 2 +- .../memory/PredicateStatisticsEstimator.java | 95 +++++++++++++++ .../impl/memory/SecondaryIndexBuilder.java | 36 ++++++ .../next/storage/impl/memory/TripleIndex.java | 110 ++---------------- 4 files changed, 139 insertions(+), 104 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/storage/impl/memory/PredicateStatisticsEstimator.java create mode 100644 src/main/java/fr/inria/corese/core/next/storage/impl/memory/SecondaryIndexBuilder.java 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 index 718ca14b9..a49d92c71 100644 --- 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 @@ -82,7 +82,7 @@ final class GraphPartition { * @return a complete graph partition with no retained staging dictionary */ static GraphPartition build(final TripleIndex spo, final Resource context, final long ordinal) { - final TripleIndex.SecondaryIndexes secondary = TripleIndex.buildSecondaryIndexes(spo); + final SecondaryIndexBuilder.Result secondary = SecondaryIndexBuilder.build(spo); final GraphPartition partition = new GraphPartition(context, ordinal, spo, secondary.pos(), secondary.osp()); spo.finishStaging(); return partition; 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/TripleIndex.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/TripleIndex.java index 90183a890..d79cae6c3 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 @@ -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. @@ -823,72 +823,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 +873,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 { + static final class Branch extends CompactTermMap { /** * Canonical first-component key retained by this root branch. */ @@ -965,7 +900,7 @@ private static final class Branch extends CompactTermMap { } /** Multi-statement leaf only; singleton statements live directly in their branch. */ - private static final class Leaf extends CompactTermMap { + static final class Leaf extends CompactTermMap { /** * Creates an empty multivalued leaf or copies the source leaf container. */ @@ -995,7 +930,7 @@ private Leaf copy() { * Receives immutable SPO statements and their canonical term keys during index construction. */ @FunctionalInterface - private interface EntryVisitor { + interface EntryVisitor { /** * Receives one immutable statement and its canonical SPO keys. * @@ -1013,7 +948,7 @@ private interface EntryVisitor { * @param visitor non-mutating callback; true requests an early stop for predicate visitors * @throws IllegalStateException if the required representation, ownership or lifecycle state is not satisfied */ - private void visitEntries(final EntryVisitor visitor) { + void visitEntries(final EntryVisitor visitor) { if (order != Order.SPO) { throw new IllegalStateException("Bulk source must be SPO"); } @@ -1096,35 +1031,4 @@ private void leaf(final TermKey object, final Statement statement) { visitor.accept(subject, predicate, object, statement); } } - - /** - * 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 SecondaryIndexes buildSecondaryIndexes(final TripleIndex source) { - final TripleIndex pos = new TripleIndex(Order.POS); - final TripleIndex osp = new 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 SecondaryIndexes(pos, osp); - } - - /** - * Completed secondary permutations sharing the SPO statements and canonical keys. - * - * @param pos completed POS permutation - * @param osp completed OSP permutation - */ - record SecondaryIndexes(/** - * Predicate/object/subject permutation of this graph. - */ - TripleIndex pos, /** - * Object/subject/predicate permutation of this graph. - */ - TripleIndex osp) { - } } From 8370866e7bc10e232c24c85c265801c23749845c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Tue, 27 Oct 2026 11:21:05 +0100 Subject: [PATCH 5/9] refactor(storage): extract TripleIndexCursor from TripleIndex - Extract positional traversal state machine into dedicated TripleIndexCursor - Update SnapshotStatementSpliterator and cursor factories - Zero SonarLint alerts and 100% tests passing --- .../memory/SnapshotStatementSpliterator.java | 2 +- .../next/storage/impl/memory/TripleIndex.java | 179 +----------------- .../impl/memory/TripleIndexCursor.java | 153 +++++++++++++++ 3 files changed, 157 insertions(+), 177 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/storage/impl/memory/TripleIndexCursor.java 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 index f841485d8..a230d1e0c 100644 --- 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 @@ -24,7 +24,7 @@ final class SnapshotStatementSpliterator implements StatementSpliterator, AutoCl private final Object monitor; private PartitionState snapshot; private Iterator partitions; - private TripleIndex.Cursor rows; + private TripleIndexCursor rows; private final StatementPattern pattern; private final TermKey subject; private final TermKey predicate; 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 d79cae6c3..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(); @@ -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. * diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/TripleIndexCursor.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/TripleIndexCursor.java new file mode 100644 index 000000000..52779cb74 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/TripleIndexCursor.java @@ -0,0 +1,153 @@ +package fr.inria.corese.core.next.storage.impl.memory; + +import fr.inria.corese.core.next.data.api.model.Statement; + +/** + * Single-consumer positional cursor over an unchanged partition retained by its caller. + */ +final class TripleIndexCursor { + + private final TripleIndex index; + private final TermKey first; + private final TermKey second; + private final TermKey third; + private int rootPosition; + private int branchPosition; + private int leafPosition; + private boolean selectedRoot; + private TripleIndex.Branch branch; + private TripleIndex.Leaf leaf; + + /** + * Captures the index and search keys for unchanged positional traversal. + * + * @param index source index containing roots and ordering + * @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 + */ + TripleIndexCursor(final TripleIndex index, final TermKey first, final TermKey second, final TermKey third) { + this.index = index; + 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 TripleIndex.Leaf values) { + leaf = values; + leafPosition = 0; + return null; + } + if (entry != null && (third == null || third.matches(index.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 + */ + @SuppressWarnings("unchecked") + private boolean selectNextRoot() { + if (first != null) { + if (selectedRoot) { + return false; + } + selectedRoot = true; + branch = index.roots.get(first); + if (branch == null) { + return false; + } + } else { + final var table = (DenseTermTable) index.roots; + final int position = table.nextIndex(rootPosition); + if (position < 0) { + return false; + } + rootPosition = position + 1; + branch = table.valueAt(position); + } + return true; + } +} From 573e930ae8b8f021371cf6567afe0f16cbcf3bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Tue, 27 Oct 2026 11:33:29 +0100 Subject: [PATCH 6/9] refactor(query): decouple SparqlAstBuilder and eliminate term/expression forwarders - Expose terms(), expressions() and paths() sub-builder accessors on SparqlAstBuilder - Update all AST listeners to call specialized builders directly - Prune 15 redundant forwarder methods and obsolete stubs from SparqlAstBuilder - Zero SonarLint alerts and 100% tests passing --- .../impl/sparql/parser/SparqlAstBuilder.java | 149 +++++------------- .../parser/SparqlQuadTemplateBuilder.java | 20 +-- .../sparql/parser/SparqlUpdateAstBuilder.java | 6 +- .../parser/listener/BgpAstListener.java | 8 +- .../parser/listener/BindAstListener.java | 4 +- .../listener/ConstructQueryAstListener.java | 16 +- .../listener/DatasetClauseAstListener.java | 4 +- .../listener/DescribeQueryAstListener.java | 4 +- .../parser/listener/FilterAstListener.java | 2 +- .../parser/listener/HavingAstListener.java | 2 +- .../listener/SelectQueryAstListener.java | 2 +- .../parser/listener/ServiceAstListener.java | 2 +- .../listener/SolutionModifierAstListener.java | 8 +- .../parser/listener/ValuesAstListener.java | 12 +- .../parser/SparqlAstBuilderDescribeTest.java | 26 +-- .../parser/SparqlAstBuilderUnionTest.java | 2 +- .../parser/SparqlQueryAstBuilderTest.java | 12 +- 17 files changed, 105 insertions(+), 174 deletions(-) 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..efccb2955 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 @@ -194,6 +194,33 @@ protected SparqlAstBuilder(SparqlParserOptions options) { this.expressions = new SparqlExpressionBuilder(this.terms, this::popCapturedExistsPattern); } + /** + * 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; + } + private boolean baseDeclarationSeen = false; public void reserveBlankNodeLabels(Collection labels) { @@ -606,90 +633,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 +669,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 +680,7 @@ public TermAst newAnonymousBlankNode() { do { label = GENERATED_BLANK_NODE_PREFIX + anonymousBlankNodeCounter++; } while (!blankNodeLabels.add(label)); - return iri(label); + return terms.iri(label); } /** @@ -772,7 +703,7 @@ public void addTriplesFromPropertyListPath( < verbSimples.get(verbSimpleIdx).getStart().getTokenIndex()); PathAst predicate = useVerbPath - ? pathFromVerbPath(verbPaths.get(verbPathIdx++)) + ? paths.pathFromVerbPath(verbPaths.get(verbPathIdx++)) : new PredicatePathAst(terms.termFromVerbSimple(verbSimples.get(verbSimpleIdx++))); for (TermAst object : termListFromObjectListPath(objectList)) { @@ -829,7 +760,7 @@ private TermAst subjectFromCollectionPath(SparqlParser.CollectionPathContext ctx */ public TermAst termFromGraphNodePath(SparqlParser.GraphNodePathContext ctx) { if (ctx.varOrTerm() != null) { - return termFromVarOrTerm(ctx.varOrTerm()); + return terms.termFromVarOrTerm(ctx.varOrTerm()); } if (ctx.triplesNodePath() != null) { return subjectFromTriplesNodePath(ctx.triplesNodePath()); @@ -858,7 +789,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 +797,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/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..dd61b2f2d 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 @@ -72,7 +72,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 +173,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/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/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlAstBuilderDescribeTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlAstBuilderDescribeTest.java index 5a6ae4219..74078f831 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlAstBuilderDescribeTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlAstBuilderDescribeTest.java @@ -67,7 +67,7 @@ class DescribeSingleIri { @Test @DisplayName("Single IRI resource is added to described list") void singleIriResource() { - builder.addDescribeResource(builder.iri("")); + builder.addDescribeResource(builder.terms().iri("")); DescribeQueryAst result = buildWithEmptyWhere(); assertFalse(result.isDescribeAll()); @@ -78,7 +78,7 @@ void singleIriResource() { @Test @DisplayName("IRI value is preserved exactly") void iriValuePreserved() { - builder.addDescribeResource(builder.iri("")); + builder.addDescribeResource(builder.terms().iri("")); DescribeQueryAst result = buildWithEmptyWhere(); IriAst iri = (IriAst) result.described().getFirst(); @@ -93,7 +93,7 @@ class DescribeSingleVariable { @Test @DisplayName("Single variable resource is added to described list") void singleVariableResource() { - builder.addDescribeResource(builder.variable("?s")); + builder.addDescribeResource(builder.terms().variable("?s")); DescribeQueryAst result = buildWithEmptyWhere(); assertFalse(result.isDescribeAll()); @@ -104,7 +104,7 @@ void singleVariableResource() { @Test @DisplayName("Variable name is stripped of ? prefix") void variableNameStripped() { - builder.addDescribeResource(builder.variable("?s")); + builder.addDescribeResource(builder.terms().variable("?s")); DescribeQueryAst result = buildWithEmptyWhere(); VarAst describedVar = (VarAst) result.described().getFirst(); @@ -114,7 +114,7 @@ void variableNameStripped() { @Test @DisplayName("$ prefix is also stripped from variable name") void dollarPrefixStripped() { - builder.addDescribeResource(builder.variable("$x")); + builder.addDescribeResource(builder.terms().variable("$x")); DescribeQueryAst result = buildWithEmptyWhere(); VarAst describedVar = (VarAst) result.described().getFirst(); @@ -129,9 +129,9 @@ class DescribeMixed { @Test @DisplayName("Multiple resources are preserved in order") void multipleResourcesPreservedInOrder() { - builder.addDescribeResource(builder.variable("?s")); - builder.addDescribeResource(builder.iri("")); - builder.addDescribeResource(builder.iri("")); + builder.addDescribeResource(builder.terms().variable("?s")); + builder.addDescribeResource(builder.terms().iri("")); + builder.addDescribeResource(builder.terms().iri("")); DescribeQueryAst result = buildWithEmptyWhere(); assertEquals(3, result.described().size()); @@ -143,7 +143,7 @@ void multipleResourcesPreservedInOrder() { @Test @DisplayName("isDescribeAll() is false when resources are present") void isDescribeAllFalseWhenResourcesPresent() { - builder.addDescribeResource(builder.variable("?s")); + builder.addDescribeResource(builder.terms().variable("?s")); DescribeQueryAst result = buildWithEmptyWhere(); assertFalse(result.isDescribeAll()); } @@ -156,7 +156,7 @@ class DescribeWithWhereClause { private DescribeQueryAst buildWithWhere() { builder.enterGroup(); builder.enterBgp(); - builder.addTriple(builder.variable("s"), builder.iri("a"), builder.iri("foaf:Person")); + builder.addTriple(builder.terms().variable("s"), builder.terms().iri("a"), builder.terms().iri("foaf:Person")); builder.exitBgp(); builder.exitGroup(); return (DescribeQueryAst) builder.getResult(); @@ -165,7 +165,7 @@ private DescribeQueryAst buildWithWhere() { @Test @DisplayName("WHERE clause is present and contains expected BGP") void whereClauseContainsBgp() { - builder.addDescribeResource(builder.variable("?s")); + builder.addDescribeResource(builder.terms().variable("?s")); DescribeQueryAst result = buildWithWhere(); assertFalse(result.whereClause().patterns().isEmpty()); @@ -175,7 +175,7 @@ void whereClauseContainsBgp() { @Test @DisplayName("BGP in WHERE clause contains the expected triple") void whereClauseBgpTriple() { - builder.addDescribeResource(builder.variable("?s")); + builder.addDescribeResource(builder.terms().variable("?s")); DescribeQueryAst result = buildWithWhere(); BgpAst bgp = (BgpAst) result.whereClause().patterns().getFirst(); @@ -186,7 +186,7 @@ void whereClauseBgpTriple() { @Test @DisplayName("Resources and WHERE clause coexist correctly") void resourcesAndWhereClauseCoexist() { - builder.addDescribeResource(builder.variable("?s")); + builder.addDescribeResource(builder.terms().variable("?s")); DescribeQueryAst result = buildWithWhere(); assertEquals(1, result.described().size()); diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlAstBuilderUnionTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlAstBuilderUnionTest.java index 8dde4ca00..ddece576b 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlAstBuilderUnionTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlAstBuilderUnionTest.java @@ -31,7 +31,7 @@ void setUp() { */ private void addTriple(String object) { builder.enterBgp(); - builder.addTriple(builder.variable("s"), builder.iri("a"), builder.iri(object)); + builder.addTriple(builder.terms().variable("s"), builder.terms().iri("a"), builder.terms().iri(object)); builder.exitBgp(); } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlQueryAstBuilderTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlQueryAstBuilderTest.java index 64e11a605..b0cc9a054 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlQueryAstBuilderTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlQueryAstBuilderTest.java @@ -75,8 +75,8 @@ void shouldBuildSingleBgpWithMultipleTriples() { b.enterSelectQuery(); b.enterGroup(); b.enterBgp(); - b.addTriple(new VarAst("s"), b.iri("a"), b.iri("foaf:Person")); - b.addTriple(new VarAst("s"), b.iri("foaf:name"), new VarAst("n")); + b.addTriple(new VarAst("s"), b.terms().iri("a"), b.terms().iri("foaf:Person")); + b.addTriple(new VarAst("s"), b.terms().iri("foaf:name"), new VarAst("n")); b.exitBgp(); b.exitGroup(); b.exitSelectQuery(); @@ -179,13 +179,13 @@ void shouldBuildLiteralTerms() { b.enterBgp(); b.addTriple( new VarAst("s"), - b.iri(""), - b.literal("\"salut\"", "fr", null) + b.terms().iri(""), + b.terms().literal("\"salut\"", "fr", null) ); b.addTriple( new VarAst("s"), - b.iri(""), - b.literal("\"12\"", null, "xsd:integer") + b.terms().iri(""), + b.terms().literal("\"12\"", null, "xsd:integer") ); b.exitBgp(); b.exitGroup(); From 181a12f54a9adf384391b25b291e86785eacf81d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Tue, 27 Oct 2026 11:42:42 +0100 Subject: [PATCH 7/9] refactor(query): extract SparqlPrologueBuilder and TriplePathExpander from SparqlAstBuilder - Extract BASE URI and PREFIX resolution into SparqlPrologueBuilder - Extract property path, collection, and blank node path expansion into TriplePathExpander - Update SparqlUpdateAstBuilder to use prologue replacement API - Zero SonarLint alerts and 100% tests passing --- .../impl/sparql/parser/SparqlAstBuilder.java | 155 ++++-------------- .../sparql/parser/SparqlPrologueBuilder.java | 82 +++++++++ .../sparql/parser/SparqlUpdateAstBuilder.java | 7 +- .../sparql/parser/TriplePathExpander.java | 141 ++++++++++++++++ 4 files changed, 258 insertions(+), 127 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlPrologueBuilder.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/TriplePathExpander.java 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 efccb2955..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,10 +188,15 @@ 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); + } + + protected SparqlPrologueBuilder createPrologueBuilder(String initialBaseUri) { + return new SparqlPrologueBuilder(initialBaseUri); } /** @@ -221,7 +226,23 @@ public SparqlExpressionBuilder expressions() { return this.expressions; } - private boolean baseDeclarationSeen = false; + /** + * 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")); @@ -230,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 --- @@ -689,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 - ? paths.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 terms.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) { 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/SparqlUpdateAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlUpdateAstBuilder.java index dd61b2f2d..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 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; + } +} From 9f58359cbc7b1db24ba60feb72a9bac4d0770573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Tue, 27 Oct 2026 13:46:35 +0100 Subject: [PATCH 8/9] refactor(query): remove obsolete ResultListener, inert reporting and dead nesting scaffolding from core.next --- .../next/query/impl/engine/eval/Eval.java | 33 ++-------- .../query/impl/engine/eval/EvalFilter.java | 3 - .../engine/eval/EvalVisitorDispatcher.java | 61 ++----------------- .../engine/event/ProcessVisitorDefault.java | 50 +-------------- .../impl/engine/event/ResultListener.java | 38 ------------ .../impl/engine/model/BindingContext.java | 13 ---- .../next/query/impl/engine/model/Edge.java | 8 --- .../query/impl/engine/pattern/QueryPlan.java | 15 ----- .../next/query/impl/engine/eval/EvalTest.java | 22 ------- .../eval/EvalVisitorDispatcherTest.java | 49 --------------- .../storage/impl/memory/LazyProducerTest.java | 14 ++--- 11 files changed, 17 insertions(+), 289 deletions(-) delete mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/engine/event/ResultListener.java 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..0f3922c32 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; @@ -724,9 +723,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 +969,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 +1038,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 +1134,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/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/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/test/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalTest.java index b0d9d693c..ce93c693a 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalTest.java @@ -16,7 +16,6 @@ import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; import fr.inria.corese.core.next.query.impl.engine.pattern.QueryPlan; import fr.inria.corese.core.next.query.impl.engine.model.BindingContext; -import fr.inria.corese.core.next.query.impl.engine.event.ResultListener; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -234,27 +233,6 @@ void testVisitorDispatcher() { } } - @Nested - @DisplayName("Result Listener Tests") - class ResultListenerTests { - - @Test - @DisplayName("Should set and get result listener") - void testSetAndGetResultListener() { - ResultListener mockListener = mock(ResultListener.class); - eval.setListener(mockListener); - assertEquals(mockListener, eval.getListener(), - "Listener should match"); - } - - @Test - @DisplayName("Should get null listener by default") - void testGetListenerDefault() { - ResultListener listener = eval.getListener(); - assertNull(listener, "Default listener should be null"); - } - } - @Nested @DisplayName("Stop Flag Tests") class StopFlagTests { diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalVisitorDispatcherTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalVisitorDispatcherTest.java index 9f08e2a8f..8bef24a4a 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalVisitorDispatcherTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalVisitorDispatcherTest.java @@ -1,20 +1,11 @@ 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 org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.Collections; -import java.util.List; - import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -36,8 +27,6 @@ void testDefaults() { assertFalse(dispatcher.hasStatement()); assertFalse(dispatcher.hasCandidate()); assertFalse(dispatcher.hasFilter()); - assertFalse(dispatcher.hasListener()); - assertNull(dispatcher.getListener()); } @Test @@ -66,42 +55,4 @@ void testNullVisitorFallback() { assertFalse(dispatcher.hasCandidate()); assertFalse(dispatcher.hasFilter()); } - - @Test - @DisplayName("Should dispatch listener calls when listener is present") - void testListenerDispatching() { - ResultListener mockListener = mock(ResultListener.class); - dispatcher.setListener(mockListener); - - assertTrue(dispatcher.hasListener()); - assertSame(mockListener, dispatcher.getListener()); - - Exp exp = mock(Exp.class); - Exp transformedExp = mock(Exp.class); - when(mockListener.listen(exp, 1)).thenReturn(transformedExp); - assertSame(transformedExp, dispatcher.listen(exp, 1)); - - Expr expr = mock(Expr.class); - dispatcher.listen(expr); - verify(mockListener).listen(expr); - - Edge qEdge = mock(Edge.class); - Edge entity = mock(Edge.class); - when(mockListener.listen(exp, qEdge, entity)).thenReturn(false); - assertFalse(dispatcher.listen(exp, qEdge, entity)); - } - - @Test - @DisplayName("Should safely bypass listener calls when listener is null") - void testListenerBypassWhenNull() { - Exp exp = mock(Exp.class); - assertSame(exp, dispatcher.listen(exp, 1)); - - Expr expr = mock(Expr.class); - assertDoesNotThrow(() -> dispatcher.listen(expr)); - - Edge qEdge = mock(Edge.class); - Edge entity = mock(Edge.class); - assertTrue(dispatcher.listen(exp, qEdge, entity)); - } } diff --git a/src/test/java/fr/inria/corese/core/next/storage/impl/memory/LazyProducerTest.java b/src/test/java/fr/inria/corese/core/next/storage/impl/memory/LazyProducerTest.java index 47d0b6077..0693ccf8b 100644 --- a/src/test/java/fr/inria/corese/core/next/storage/impl/memory/LazyProducerTest.java +++ b/src/test/java/fr/inria/corese/core/next/storage/impl/memory/LazyProducerTest.java @@ -8,7 +8,7 @@ import fr.inria.corese.core.next.query.impl.engine.eval.Eval; import fr.inria.corese.core.next.query.impl.engine.eval.RdfTermMatcher; import fr.inria.corese.core.next.query.impl.engine.eval.SparqlKgramEvaluator; -import fr.inria.corese.core.next.query.impl.engine.event.ResultListener; +import fr.inria.corese.core.next.query.impl.engine.spi.Matcher; import fr.inria.corese.core.next.query.impl.engine.model.Edge; import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; import fr.inria.corese.core.next.query.impl.engine.spi.Environment; @@ -161,16 +161,14 @@ else try (var result = f.executor.evaluateTuple("SELECT * { ?s ?p ?o } LIMIT " + } } - @Test void matcherOrListenerFailureClosesTheCurrentlySuspendedEdgeCursor() throws Exception { + @Test void matcherFailureClosesTheCurrentlySuspendedEdgeCursor() throws Exception { try (var f = new Fixture()) { var producer = new StorageManagerProducer(f.observed); - var eval = Eval.create(producer, new SparqlKgramEvaluator(), new RdfTermMatcher()); + Matcher matcher = mock(Matcher.class); + when(matcher.match(any(Edge.class), any(Edge.class), any(Environment.class))) + .thenThrow(new IllegalStateException("matcher failed")); + var eval = Eval.create(producer, new SparqlKgramEvaluator(), matcher); var query = new CoreseAstQueryBuilder().toNextQuery((SelectQueryAst) new SparqlParser().parse("SELECT * { ?s ?p ?o }")); - ResultListener listener = mock(ResultListener.class); - when(listener.listen(any(Exp.class), anyInt())).thenAnswer(call -> call.getArgument(0)); - when(listener.listen(any(Exp.class), any(Edge.class), any(Edge.class))) - .thenThrow(new IllegalStateException("listener failed")); - eval.setListener(listener); assertThrows(IllegalStateException.class, () -> eval.query(query)); assertEquals(1, f.read.get()); f.assertReleased(); } From 61e992eebe8748b5baf30bcd089845d7f6f25c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Tue, 27 Oct 2026 15:04:41 +0100 Subject: [PATCH 9/9] refactor(architecture): decouple query from CoreseIO, enforce boundary test, and eliminate dead opcodes and evaluator hooks --- .../core/next/query/api/io/package-info.java | 4 +--- .../next/query/impl/engine/eval/Eval.java | 1 - .../engine/eval/SparqlKgramEvaluator.java | 11 ---------- .../next/query/impl/engine/model/ExpType.java | 18 +++------------ .../query/impl/engine/model/ExprType.java | 22 ------------------- .../next/query/impl/engine/pattern/Exp.java | 11 +++++----- .../impl/engine/pattern/ExpPatternScope.java | 5 ----- .../impl/engine/pattern/QueryExpIndexer.java | 7 +----- .../impl/engine/planning/QueryPlanner.java | 2 +- .../query/impl/engine/solution/Memory.java | 9 +------- .../next/query/impl/engine/spi/Evaluator.java | 17 -------------- .../next/query/impl/query/package-info.java | 2 +- .../CoreseRepositoryConnection.java | 11 +++++++--- .../impl/sparql/update/UpdateLoader.java | 4 ++-- .../architecture/NextModuleBoundaryTest.java | 7 ++++++ 15 files changed, 30 insertions(+), 101 deletions(-) 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 0f3922c32..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 @@ -108,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); 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/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/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 @@ *
  • {@link fr.inria.corese.core.next.query.impl.query.CoreseTupleQuery} — SELECT
  • *
  • {@link fr.inria.corese.core.next.query.impl.query.CoreseBooleanQuery} — ASK
  • *
  • {@link fr.inria.corese.core.next.query.impl.query.CoreseGraphQuery} — CONSTRUCT / DESCRIBE
  • - *
  • {@link fr.inria.corese.core.next.query.impl.query.CoreseUpdate} — SPARQL UPDATE (INSERT DATA / DELETE DATA)
  • + *
  • {@link fr.inria.corese.core.next.query.impl.query.CoreseUpdate} — SPARQL 1.1 Update (INSERT/DELETE DATA, DELETE/INSERT WHERE, Graph Management, LOAD)
  • * * *

    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/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/test/java/fr/inria/corese/core/next/architecture/NextModuleBoundaryTest.java b/src/test/java/fr/inria/corese/core/next/architecture/NextModuleBoundaryTest.java index e5686be1a..63280de09 100644 --- a/src/test/java/fr/inria/corese/core/next/architecture/NextModuleBoundaryTest.java +++ b/src/test/java/fr/inria/corese/core/next/architecture/NextModuleBoundaryTest.java @@ -68,6 +68,13 @@ void storageMustNotDependOnQuery() throws IOException { imported -> imported.startsWith("fr.inria.corese.core.next.query.")); } + @Test + void queryMustNotDependOnIoFacade() throws IOException { + assertNoReferences( + NEXT_SOURCES.resolve("query"), + imported -> imported.startsWith("fr.inria.corese.core.next.io.")); + } + @Test void nextMustNotDependOnAnyHistoricalCoresePackage() throws IOException { assertNoReferences(NEXT_SOURCES,