diff --git a/build.gradle.kts b/build.gradle.kts index 8db4fa9fb..9b4839b89 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,6 +59,74 @@ sourceSets { } } +// Compile and test the native implementation without historical Corese classes +// on either classpath. No main output or JavaCC sources are included here. +val nextSources = sourceSets.create("next") { + java.setSrcDirs(listOf("src/main/java", antlrGeneratedDir)) + java.include("fr/inria/corese/core/next/**/*.java") + resources.setSrcDirs(listOf("src/main/resources")) + resources.include("META-INF/services/fr.inria.corese.core.next.storage.api.plugin.StoragePlugin") +} + +val nextTestSources = sourceSets.create("nextTest") { + java.setSrcDirs(listOf("src/test/java")) + java.include("fr/inria/corese/core/next/**/*.java") + resources.setSrcDirs(listOf("src/test/resources")) + compileClasspath = nextSources.output + configurations[compileClasspathConfigurationName] + runtimeClasspath = output + nextSources.output + configurations[runtimeClasspathConfigurationName] +} + +// Native sources and tests define their own dependency graph. The historical +// build inherits those dependencies, while the native build never inherits its output or libraries. +configurations.implementation.get().extendsFrom(configurations[nextSources.implementationConfigurationName]) +configurations[nextTestSources.implementationConfigurationName] + .extendsFrom(configurations[nextSources.implementationConfigurationName]) +configurations.testImplementation.get().extendsFrom(configurations[nextTestSources.implementationConfigurationName]) +configurations.testRuntimeOnly.get().extendsFrom(configurations[nextTestSources.runtimeOnlyConfigurationName]) + +tasks.named(nextSources.compileJavaTaskName) { + dependsOn("generateGrammarSource") + options.encoding = "UTF-8" + options.compilerArgs.addAll(listOf("-proc:none", "-Xlint:all", "-Werror")) +} + +val nextTest by tasks.registering(Test::class) { + description = "Tests the native implementation with no historical Corese classes available." + group = "verification" + testClassesDirs = nextTestSources.output.classesDirs + classpath = nextTestSources.runtimeClasspath + useJUnitPlatform() + systemProperty("corese.next.isolated", "true") + systemProperty("java.util.logging.config.file", "src/test/resources/logging.properties") +} + +val nextJar by tasks.registering(Jar::class) { + description = "Builds the isolated native classes and their storage plugin descriptor." + group = "build" + archiveClassifier.set("next") + from(nextSources.output) +} + +val nextJavadoc by tasks.registering(Javadoc::class) { + description = "Checks the native API documentation without historical Corese classes." + group = "documentation" + source = nextSources.allJava.matching { exclude("**/generated/**") } + classpath = nextSources.output + nextSources.compileClasspath + destinationDir = layout.buildDirectory.dir("docs/next-javadoc").get().asFile + options.encoding = "UTF-8" + (options as StandardJavadocDocletOptions).addStringOption("Xdoclint:all,-missing", "-quiet") +} + +tasks.register("checkNext") { + description = "Compiles, packages, and tests next independently of historical Corese classes." + group = "verification" + dependsOn(nextTest, nextJar, nextJavadoc) +} + +tasks.named("check") { + dependsOn(nextSources.compileJavaTaskName) +} + // Ensure JavaCC generation happens before compilation tasks.named("compileJava") { dependsOn("javaccSparqlCorese") @@ -111,6 +179,14 @@ repositories { } dependencies { + // === Standalone native runtime === + add(nextSources.implementationConfigurationName, "org.slf4j:slf4j-api:2.0.17") + add(nextSources.implementationConfigurationName, "org.antlr:antlr4-runtime:4.13.2") + add(nextSources.implementationConfigurationName, "com.apicatalog:titanium-json-ld:1.6.0") + add(nextSources.implementationConfigurationName, "com.apicatalog:titanium-rdf-api:1.0.0") + add(nextSources.implementationConfigurationName, "org.eclipse.parsson:parsson:1.1.7") + add(nextSources.implementationConfigurationName, "jakarta.json:jakarta.json-api:2.1.3") + add(nextSources.implementationConfigurationName, "com.typesafe:config:1.4.3") // === Logging === // As a library, corese-core should only depend on the SLF4J API. // The actual logging implementation (Logback, Log4j2, etc.) should be chosen @@ -124,20 +200,12 @@ dependencies { // === Antlr dependencies === antlr("org.antlr:antlr4:4.13.2") // Antlr for parsing (ANTLR 4) - implementation("org.antlr:antlr4-runtime:4.13.2") // Antlr runtime for parsing // === HTTP and XML === implementation("org.glassfish.jersey.core:jersey-client:3.1.10") // HTTP client (Jersey) implementation("org.glassfish.jersey.inject:jersey-hk2:3.1.10") // Dependency injection for Jersey implementation("com.sun.activation:jakarta.activation:2.0.1") // MIME type handling (Jakarta Activation) - // === JSONLD Parsing === - implementation("com.apicatalog:titanium-json-ld:1.6.0") // JSON-LD processing library - implementation("com.apicatalog:titanium-rdf-api:1.0.0") // Titanium RDF API for JSON-LD processing - implementation("org.eclipse.parsson:parsson:1.1.7") // JSON parser for JSON-LD - implementation("jakarta.json:jakarta.json-api:2.1.3") // Jakarta JSON API for JSON processing - - // === XML parsing === implementation("com.typesafe.akka:akka-stream_2.13:2.6.20") // Akka Streams for reactive streams processing implementation("com.lightbend.akka:akka-stream-alpakka-xml_2.13:3.0.4") // Alpakka XML for XML processing with Akka Streams @@ -148,16 +216,15 @@ dependencies { // === Utilities === implementation("org.apache.commons:commons-text:1.13.1") // Text manipulation utilities (Commons Text) implementation("org.json:json:20250517") // JSON processing - implementation("com.typesafe:config:1.4.3") // Configuration library (Typesafe Config) // === Test dependencies === - testImplementation(platform("org.junit:junit-bom:5.13.2")) // JUnit BOM for consistent test versions - testImplementation("org.junit.jupiter:junit-jupiter:5.13.2") // JUnit Jupiter API and engine - testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.13.2") // JUnit platform launcher (runtime) - testImplementation("org.mockito:mockito-core:5.18.0") // Mockito core for mocking in tests - testImplementation("org.mockito:mockito-junit-jupiter:5.18.0") // Mockito integration with JUnit Jupiter - testRuntimeOnly("org.apache.logging.log4j:log4j-core:2.25.0") // Log4j2 core for internal logging - testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.25.0") // SLF4J binding for Log4j2 (runtime) + add(nextTestSources.implementationConfigurationName, platform("org.junit:junit-bom:5.13.2")) + add(nextTestSources.implementationConfigurationName, "org.junit.jupiter:junit-jupiter:5.13.2") + add(nextTestSources.runtimeOnlyConfigurationName, "org.junit.platform:junit-platform-launcher:1.13.2") + add(nextTestSources.implementationConfigurationName, "org.mockito:mockito-core:5.18.0") + add(nextTestSources.implementationConfigurationName, "org.mockito:mockito-junit-jupiter:5.18.0") + add(nextTestSources.runtimeOnlyConfigurationName, "org.apache.logging.log4j:log4j-core:2.25.0") + add(nextTestSources.runtimeOnlyConfigurationName, "org.apache.logging.log4j:log4j-slf4j2-impl:2.25.0") } diff --git a/docs/memory-storage-final-validation.md b/docs/memory-storage-final-validation.md index fb41df884..33996829f 100644 --- a/docs/memory-storage-final-validation.md +++ b/docs/memory-storage-final-validation.md @@ -6,7 +6,7 @@ conservées. ## Timeout et transactions -`NextSparqlPipelineExecutor` exécute toujours l'évaluation sur le thread qui possède +`SparqlQueryExecutor` exécute toujours l'évaluation sur le thread qui possède la transaction. Le watchdog utilise désormais un seul `AtomicInteger` : le premier CAS départage fin d'évaluation et expiration, sans intervalle entre la revendication d'un timeout et sa publication. Un résultat ne peut pas être accepté après que le diff --git a/docs/next-engine-ast-boundary.md b/docs/next-engine-ast-boundary.md index 0da162598..eba71ebbd 100644 --- a/docs/next-engine-ast-boundary.md +++ b/docs/next-engine-ast-boundary.md @@ -10,7 +10,7 @@ Previously, three source files in `next/query/impl/engine` retained AST dependen - `model/Filter.java`: returned `TermAst` via `getFilterExpression()` and `coreseNextSource()`. These AST accessors were removed from the engine interface; the bridge implementation - `NextFilterFromAst` retains AST-level access internally where needed. + `AstBackedFilter` retains AST-level access internally where needed. - `pattern/Exp.java`: exposed `getFilterExpression()` delegating to `Filter`. Removed as it had no engine callers. - `pattern/Query.java`: retained `QueryAst ast` with `getAST()`, `getGlobalAST()`, and `setAST()`. @@ -18,7 +18,7 @@ Previously, three source files in `next/query/impl/engine` retained AST dependen ## Guard behavior -- Zero exceptions: `EXISTING_ENGINE_AST_DEPENDENCIES` is empty. +- The engine dependency set must be empty; no allowlist or exception mechanism exists. - Parser dependencies have no exceptions. - Both the actual `query.impl.sparql.ast` package and the historical `query.impl.ast` package are checked. @@ -33,3 +33,17 @@ Previously, three source files in `next/query/impl/engine` retained AST dependen ./gradlew test --tests '*NextModuleBoundaryTest' ./gradlew check -x test ``` + +## Syntax-tree ownership + +AST traversal contracts (`AstVisitor`, `AbstractAstVisitor`, and `VisitableAst`) +reside in `sparql.ast.visitor`. Semantic validation depends on the syntax tree; +the syntax tree cannot depend on its parser or the execution engine. The same +boundary test protects this direction, including fully qualified references. + +## Independent verification + +`./gradlew checkNext --offline` compiles, documents, packages, and tests native +sources with their own dependency graph. Historical Corese classes, the JavaCC +parser, and historical runtime libraries are absent from the native classpaths. +`compileNextJava` treats every Java 21 compiler warning as an error. diff --git a/docs/source/next_repository.md b/docs/source/next_repository.md index 0a8e50bf7..3fda77bef 100644 --- a/docs/source/next_repository.md +++ b/docs/source/next_repository.md @@ -96,6 +96,34 @@ try (Repository repository = Repositories.create(); } ``` +Native SELECT results stream basic patterns, filters, BIND, VALUES, pure grouped +joins, supported UNION and GRAPH scopes, and simple OPTIONAL patterns. They keep +only the current bindings and row, rather than accumulating all result mappings. +Aggregates, paths, services, subqueries, MINUS, and joins requiring a +buffered strategy retain full evaluation. A scoped branch also keeps full evaluation +when repeatedly rescanning it would introduce quadratic work. DISTINCT retains its +required set of previously seen projections. + +For eligible ordered SELECTs with a finite LIMIT, online Top-K selection retains +at most LIMIT + OFFSET mappings instead of buffering every solution. It uses the +Corese comparator and preserves arrival order for ties. All candidates are still +evaluated before results are delivered; DISTINCT additionally retains its seen +set. Snapshot ownership, interruption and deadlines cover both scanning and final +ranking. Queries without LIMIT, unsupported input algebra, and bounds exceeding +the supported integer capacity keep the classic full evaluator. + +Consume a progressive result on the thread that called `evaluate()`, and always +close it with try-with-resources, including after `stream().limit(...)`. Its independent +read snapshot includes transaction-local writes visible at evaluation, and stays stable +across later writes, commits and rollbacks. Exhaustion, SPARQL LIMIT, failure and close +release the snapshot without finishing the source transaction. Cross-thread close can +cancel a cursor. A configured timeout includes client idle time; expiration releases +an idle snapshot without interrupting unrelated client code. Lazy failures are reported +by `hasNext()` or `next()`. + +Backends without independent read snapshots use full evaluation. The repository-level +`select` shortcut remains materialized as described above. + Closing a connection never closes its repository. Closing the repository makes all its connections unusable. @@ -134,12 +162,90 @@ Prepared updates intentionally expose only `execute()` today. Bindings, datasets, inference switches, and timeouts are not advertised for updates until the execution pipeline can honor their complete semantics. +Prepared queries retain an immutable compilation image after their first successful +planning pass. The first evaluation uses the compiler's fresh objects after a detached image has +been captured; subsequent evaluations create independent runtime operators, variable +indexes, VALUES rows, expression adapters and EXISTS caches. Execution never uses +the cached image itself. Changing bindings or the timeout reuses this plan. Changing the +ordered dataset or the session-visible storage version rebuilds it using current +cardinalities. Effective mutations, including writes inside a transaction, change +that version; no-op mutations do not. A snapshot transaction retains its version +across other sessions' commits. Its own commit preserves an unchanged visible plan, +while rollback restores the version of the data then visible outside the transaction. +Backends returning an unavailable data version are replanned on every evaluation. +A version change during planning prevents publication of that cache entry; concurrent +writes can still age estimates during execution, without changing query semantics. + +## Execution diagnostics + +Call `evaluateWithDiagnostics()` on a prepared SELECT, ASK, CONSTRUCT or DESCRIBE +query to execute it once and inspect immutable snapshots of its progress: + +```java +try (RepositoryConnection connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery( + "SELECT ?s { ?s . ?s \"Alice\" }") + .evaluateWithDiagnostics(); + try (TupleQueryResult result = execution.result()) { + result.forEach(System.out::println); + } + System.out.print(execution.profile().format()); +} +``` + +The profile reports AST-to-runtime compilation, plan completion (ordering, +statistics and node indexing), execution-frame creation, and evaluation durations. +`planReused()` identifies a cache hit: compilation and planning times are then zero, +while execution-frame creation remains measured. The initial compilation duration +includes capture of the private compilation image. Parsing, client idle time +and graph template materialization are excluded. Timings include +instrumentation overhead. The ordered patterns retain their operator locations; +this is a structural plan, not a chronological trace across UNION or OPTIONAL. +Runtime path lookups and dynamically compiled EXISTS patterns are marked as +auxiliary, with identical auxiliary descriptions aggregated. + +`profile.delivery()` describes the strategy actually used: `STREAMING` or +`MATERIALIZED`. Its `fallbackReason()` reports the first guard selecting full +evaluation, such as `ORDER_BY`, `AGGREGATION`, `PROPERTY_PATH` or +`SNAPSHOT_UNAVAILABLE`; `NONE` means a progressive cursor was opened. This is +the selected guard, not a complete list of blocking operators. + +For SELECT, `delivery().firstRowStatus()` starts as `UNOBSERVED` until a row is +available (`AVAILABLE`) or successful evaluation establishes an empty result +after OFFSET and LIMIT (`EMPTY`). Materialized SELECT results have already reached +one of these terminal observations when evaluation returns. Closing a cursor +without advancing it, or failing before the first row, leaves it `UNOBSERVED`. +For ASK, CONSTRUCT and DESCRIBE, the status is `NOT_APPLICABLE` and the fallback +reason is `QUERY_FORM`. + +`firstRowNanos()` is non-negative only for `AVAILABLE`, and otherwise returns +`-1`. It accumulates active wall-clock time from the profiled evaluation until a +row is ready: before evaluation returns for materialized results, or on the first +successful `hasNext()` or `next()` for progressive results. Compilation, planning +and cursor setup are included; client pauses and initial query preparation are excluded. Once +a row is ready, this duration remains fixed even after further reading or closure. + +For each pattern, the profile records the estimate already requested by the +planner (`-1` when unavailable), producer iterator openings, and candidate edges +returned to the engine after graph deduplication and before RDF matching. +Candidates are summed across invocations: they are neither result-row counts nor +internal index visits. Estimates precede runtime bindings and may overestimate +merged-graph cardinalities. Diagnostics do not request additional counts or scans. + +Ordinary `evaluate()` calls use the uninstrumented producer and cursors. +Diagnostics preserve dataset, bindings, timeout and resource ownership. The caller +must close tuple and graph results. For progressive SELECT, `execution.profile()` +reports work performed so far; consume or close the result before reading the final +profile. Every returned profile remains immutable. Lazy failures retain their usual +exceptions and release the cursor's resources. + ## Transactions Call `supportsTransactions()` before `begin()`. `isActive()` reports the state of the transaction owned by the connection. `commit()` and `rollback()` require an active transaction, and closing a connection rolls one back automatically. -The standard in-memory backend currently reports transactions as unsupported. +The native in-memory backend supports snapshot transactions with optimistic +conflict detection at commit. ## SPARQL feature coverage diff --git a/src/main/java/fr/inria/corese/core/next/common/exception/ConfigurationException.java b/src/main/java/fr/inria/corese/core/next/common/exception/ConfigurationException.java index 7cbed6659..53073277e 100644 --- a/src/main/java/fr/inria/corese/core/next/common/exception/ConfigurationException.java +++ b/src/main/java/fr/inria/corese/core/next/common/exception/ConfigurationException.java @@ -4,6 +4,9 @@ * Class for any exception related to the configuration of Corese. */ public class ConfigurationException extends CoreseException { + @java.io.Serial + private static final long serialVersionUID = 1L; + public ConfigurationException(String msg) { super(msg); diff --git a/src/main/java/fr/inria/corese/core/next/data/api/exception/SerializationException.java b/src/main/java/fr/inria/corese/core/next/data/api/exception/SerializationException.java index 4a587dfe0..dfa9275e4 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/exception/SerializationException.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/exception/SerializationException.java @@ -11,6 +11,9 @@ * stream. */ public class SerializationException extends CoreseException { + @java.io.Serial + private static final long serialVersionUID = 1L; + private final String formatName; private final int lineNumber; private final int columnNumber; diff --git a/src/main/java/fr/inria/corese/core/next/data/api/model/RdfValueOrder.java b/src/main/java/fr/inria/corese/core/next/data/api/model/RdfValueOrder.java index 345f279c6..26c104af5 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/model/RdfValueOrder.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/model/RdfValueOrder.java @@ -1,11 +1,13 @@ package fr.inria.corese.core.next.data.api.model; +import fr.inria.corese.core.next.data.Values; import fr.inria.corese.core.next.data.api.literal.CoreDatatype; import fr.inria.corese.core.next.data.api.literal.RDFDatatype; import fr.inria.corese.core.next.data.api.literal.XSDDatatype; 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.SimpleLiteral; import fr.inria.corese.core.next.data.api.term.Triple; import java.math.BigDecimal; @@ -109,10 +111,12 @@ private static int compareLiterals(Literal left, Literal right) { private static int compareCompatibleValues(Literal left, Literal right) { if (isNumeric(left) && isNumeric(right)) { + Literal numericLeft = numericValue(left); + Literal numericRight = numericValue(right); if (isFloatingPoint(left) || isFloatingPoint(right)) { - return compareFloatingPoint(left.doubleValue(), right.doubleValue()); + return compareFloatingPoint(numericLeft.doubleValue(), numericRight.doubleValue()); } - return decimalValue(left).compareTo(decimalValue(right)); + return decimalValue(numericLeft).compareTo(decimalValue(numericRight)); } if (isBoolean(left) && isBoolean(right)) { return Boolean.compare(left.booleanValue(), right.booleanValue()); @@ -120,6 +124,12 @@ private static int compareCompatibleValues(Literal left, Literal right) { return left.getLabel().compareTo(right.getLabel()); } + /** Generic typed literals retain their RDF spelling but have no numeric accessors. */ + private static Literal numericValue(Literal literal) { + return literal instanceof SimpleLiteral + ? Values.factory().createLiteral(literal.getLabel(), literal.getDatatype()) : literal; + } + private static int compareFloatingPoint(double left, double right) { if (Double.isNaN(left)) { return Double.isNaN(right) ? 0 : -1; diff --git a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDate.java b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDate.java index 5b1997eba..41aa70f06 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDate.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDate.java @@ -20,6 +20,7 @@ public final class SimpleDate extends AbstractTemporalPointLiteral { private static final long serialVersionUID = 1L; private final String label; + @SuppressWarnings("serial") // Calendar implementations have VM-specific serialization; RDF is the portable representation. private final XMLGregorianCalendar calendar; public SimpleDate(String lexicalValue) { diff --git a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDateTime.java b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDateTime.java index 957ff29fa..c2277b83a 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDateTime.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDateTime.java @@ -20,6 +20,7 @@ public final class SimpleDateTime extends AbstractTemporalPointLiteral { private static final long serialVersionUID = 1L; private final String label; + @SuppressWarnings("serial") // Calendar implementations have VM-specific serialization; RDF is the portable representation. private final XMLGregorianCalendar calendar; public SimpleDateTime(String lexicalValue) { diff --git a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleTime.java b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleTime.java index cfa0701df..219e29685 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleTime.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleTime.java @@ -20,6 +20,7 @@ public final class SimpleTime extends AbstractTemporalPointLiteral { private static final long serialVersionUID = 1L; private final String label; + @SuppressWarnings("serial") // Calendar implementations have VM-specific serialization; RDF is the portable representation. private final XMLGregorianCalendar calendar; public SimpleTime(String lexicalValue) { diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsListener.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsListener.java index 3317f1c28..6cafd8fcd 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsListener.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsListener.java @@ -3,9 +3,8 @@ import fr.inria.corese.core.next.data.api.term.*; import fr.inria.corese.core.next.data.api.model.*; import fr.inria.corese.core.next.data.api.factory.ValueFactory; -import fr.inria.corese.core.next.data.api.io.option.IOOptions; import fr.inria.corese.core.next.data.api.exception.ParsingException; -import fr.inria.corese.core.next.data.impl.io.parser.support.AbstractNTriplesNQuadsListener; +import fr.inria.corese.core.next.data.impl.io.parser.support.NTriplesQuadsTermDecoder; import fr.inria.corese.core.next.generated.antlr.NQuadsBaseListener; import fr.inria.corese.core.next.generated.antlr.NQuadsParser; @@ -16,7 +15,7 @@ */ public class NQuadsListener extends NQuadsBaseListener { - private final AbstractNTriplesNQuadsListener abstractNTriplesQuadsListener; + private final NTriplesQuadsTermDecoder termDecoder; private final Model model; private final ValueFactory factory; @@ -29,20 +28,16 @@ public class NQuadsListener extends NQuadsBaseListener { * * @param model RDF model to populate * @param factory ValueFactory for creating RDF resources - * @param options IO configuration options */ - public NQuadsListener(Model model, ValueFactory factory, IOOptions options) { + public NQuadsListener(Model model, ValueFactory factory) { this.model = model; this.factory = factory; - this.abstractNTriplesQuadsListener = new AbstractNTriplesNQuadsListener(model, factory, options) { - }; + this.termDecoder = new NTriplesQuadsTermDecoder(factory); } @Override public void enterStatement(NQuadsParser.StatementContext ctx) { - - currentSubject = extractSubject(ctx.subject()); currentPredicate = extractPredicate(ctx.predicate()); currentGraph = (ctx.graphLabel() != null) ? extractGraph(ctx.graphLabel()) : null; @@ -68,7 +63,7 @@ public void exitStatement(NQuadsParser.StatementContext ctx) { */ protected Resource extractSubject(NQuadsParser.SubjectContext ctx) { if (ctx.IRIREF() != null) { - String iri = abstractNTriplesQuadsListener.unescapeUri(stripAngles(ctx.IRIREF().getText())); + String iri = termDecoder.unescapeUri(stripAngles(ctx.IRIREF().getText())); return factory.createIRI(iri); } if (ctx.BLANK_NODE_LABEL() != null) { @@ -84,7 +79,7 @@ protected Resource extractSubject(NQuadsParser.SubjectContext ctx) { */ protected IRI extractPredicate(NQuadsParser.PredicateContext ctx) { if (ctx.IRIREF() != null) { - String iri = abstractNTriplesQuadsListener.unescapeUri(stripAngles(ctx.IRIREF().getText())); + String iri = termDecoder.unescapeUri(stripAngles(ctx.IRIREF().getText())); return factory.createIRI(iri); } throw new ParsingException("Unsupported N-Quads predicate: " + ctx.getText()); @@ -95,7 +90,7 @@ protected IRI extractPredicate(NQuadsParser.PredicateContext ctx) { */ protected Value extractObject(NQuadsParser.ObjectContext ctx) { if (ctx.IRIREF() != null) { - String iri = abstractNTriplesQuadsListener.unescapeUri(stripAngles(ctx.IRIREF().getText())); + String iri = termDecoder.unescapeUri(stripAngles(ctx.IRIREF().getText())); return factory.createIRI(iri); } if (ctx.BLANK_NODE_LABEL() != null) { @@ -114,7 +109,7 @@ protected Value extractObject(NQuadsParser.ObjectContext ctx) { */ protected Resource extractGraph(NQuadsParser.GraphLabelContext ctx) { if (ctx.IRIREF() != null) { - String iri = abstractNTriplesQuadsListener.unescapeUri(stripAngles(ctx.IRIREF().getText())); + String iri = termDecoder.unescapeUri(stripAngles(ctx.IRIREF().getText())); return factory.createIRI(iri); } if (ctx.BLANK_NODE_LABEL() != null) { @@ -137,26 +132,26 @@ protected Literal extractLiteral(NQuadsParser.LiteralContext ctx) { throw new ParsingException("Missing literal token: " + ctx.getText()); } - String label = abstractNTriplesQuadsListener.unescapeLiteral(rawText); + String label = termDecoder.unescapeLiteral(rawText); IRI datatype = null; String langTag = null; if (ctx.IRIREF() != null) { - String iri = abstractNTriplesQuadsListener.unescapeUri(stripAngles(ctx.IRIREF().getText())); + String iri = termDecoder.unescapeUri(stripAngles(ctx.IRIREF().getText())); datatype = factory.createIRI(iri); } else if (ctx.LANGTAG() != null) { langTag = ctx.LANGTAG().getText().substring(1); } - return abstractNTriplesQuadsListener.createLiteral(label, datatype, langTag); + return termDecoder.createLiteral(label, datatype, langTag); } /** * Strips angle brackets from an IRI reference. */ private String stripAngles(String iriRef) { - return abstractNTriplesQuadsListener.stripAngles(iriRef); + return termDecoder.stripAngles(iriRef); } /** @@ -167,34 +162,11 @@ private String stripAngles(String iriRef) { * @throws ParsingException if the blank node label is invalid. */ protected void validateBlankNodeLabel(String label) { - abstractNTriplesQuadsListener.validateBlankNodeLabel(label); + termDecoder.validateBlankNodeLabel(label); if (!label.matches("^\\w[\\w\\-\\.]*$")) { throw new ParsingException("Invalid blank node label syntax: " + label); } } - /** - * Unescapes common N-Quads literal escape sequences. - * - * @param literalText The raw literal string from ANTLR (including quotes) - * @return The unescaped literal string without surrounding quotes - * @deprecated Use helper.unescapeLiteral instead - */ - @Deprecated(since = "4.5.0", forRemoval = false) - protected String unescapeLiteral(String literalText) { - return abstractNTriplesQuadsListener.unescapeLiteral(literalText); - } - - /** - * Unescapes common N-Quads URI escape sequences. - * - * @param uri The escaped URI string - * @return The unescaped URI string - * @deprecated Use helper.unescapeUri instead - */ - @Deprecated(since = "4.5.0", forRemoval = false) - protected String unescapeUri(String uri) { - return abstractNTriplesQuadsListener.unescapeUri(uri); - } } diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsParser.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsParser.java index 76dcee7fc..6c9d44b2c 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsParser.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsParser.java @@ -71,7 +71,7 @@ public void parse(Reader reader, String baseURI) throws ParsingException { fr.inria.corese.core.next.generated.antlr.NQuadsParser parser = new fr.inria.corese.core.next.generated.antlr.NQuadsParser(tokens); configureErrorHandling(parser); - NQuadsListener listener = new NQuadsListener(getModel(), getValueFactory(), getConfig()); + NQuadsListener listener = new NQuadsListener(getModel(), getValueFactory()); LineRDFParser.parse(reader, lexer, parser, parser::nquadsDoc, listener); } catch (ParsingException e) { diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesListener.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesListener.java index 606b960e0..5cbb605b6 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesListener.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesListener.java @@ -6,9 +6,8 @@ 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.data.api.factory.ValueFactory; -import fr.inria.corese.core.next.data.api.io.option.IOOptions; import fr.inria.corese.core.next.data.api.exception.ParsingException; -import fr.inria.corese.core.next.data.impl.io.parser.support.AbstractNTriplesNQuadsListener; +import fr.inria.corese.core.next.data.impl.io.parser.support.NTriplesQuadsTermDecoder; import fr.inria.corese.core.next.generated.antlr.NTriplesBaseListener; import fr.inria.corese.core.next.generated.antlr.NTriplesParser; @@ -19,7 +18,7 @@ */ public class NTriplesListener extends NTriplesBaseListener { - private final AbstractNTriplesNQuadsListener abstractNTriplesQuadsListener; + private final NTriplesQuadsTermDecoder termDecoder; private final Model model; private final ValueFactory factory; @@ -31,12 +30,11 @@ public class NTriplesListener extends NTriplesBaseListener { * * @param model The RDF model to populate. * @param factory The ValueFactory for creating RDF resources. - * @param options IOOptions for configuration (if any). */ - public NTriplesListener(Model model, ValueFactory factory, IOOptions options) { + public NTriplesListener(Model model, ValueFactory factory) { this.model = model; this.factory = factory; - this.abstractNTriplesQuadsListener = new AbstractNTriplesNQuadsListener(model, factory, options) {}; + this.termDecoder = new NTriplesQuadsTermDecoder(factory); } @Override @@ -58,12 +56,12 @@ public void exitTriple(NTriplesParser.TripleContext ctx) { */ public Resource extractSubject(NTriplesParser.SubjectContext ctx) { if (ctx.IRIREF() != null) { - String iri = abstractNTriplesQuadsListener.unescapeUri(abstractNTriplesQuadsListener.stripAngles(ctx.IRIREF().getText())); + String iri = termDecoder.unescapeUri(termDecoder.stripAngles(ctx.IRIREF().getText())); return factory.createIRI(iri); } if (ctx.BLANK_NODE_LABEL() != null) { - String label = abstractNTriplesQuadsListener.extractBlankNodeLabel(ctx.BLANK_NODE_LABEL().getText()); - abstractNTriplesQuadsListener.validateBlankNodeLabel(label); + String label = termDecoder.extractBlankNodeLabel(ctx.BLANK_NODE_LABEL().getText()); + termDecoder.validateBlankNodeLabel(label); return factory.createBNode(label); } throw new ParsingException("Unsupported N-Triples subject: " + ctx.getText()); @@ -74,7 +72,7 @@ public Resource extractSubject(NTriplesParser.SubjectContext ctx) { */ public IRI extractPredicate(NTriplesParser.PredicateContext ctx) { if (ctx.IRIREF() != null) { - String iri = abstractNTriplesQuadsListener.unescapeUri(abstractNTriplesQuadsListener.stripAngles(ctx.IRIREF().getText())); + String iri = termDecoder.unescapeUri(termDecoder.stripAngles(ctx.IRIREF().getText())); return factory.createIRI(iri); } throw new ParsingException("Unsupported N-Triples predicate: " + ctx.getText()); @@ -85,12 +83,12 @@ public IRI extractPredicate(NTriplesParser.PredicateContext ctx) { */ public Value extractObject(NTriplesParser.ObjectContext ctx) { if (ctx.IRIREF() != null) { - String iri = abstractNTriplesQuadsListener.unescapeUri(abstractNTriplesQuadsListener.stripAngles(ctx.IRIREF().getText())); + String iri = termDecoder.unescapeUri(termDecoder.stripAngles(ctx.IRIREF().getText())); return factory.createIRI(iri); } if (ctx.BLANK_NODE_LABEL() != null) { - String label = abstractNTriplesQuadsListener.extractBlankNodeLabel(ctx.BLANK_NODE_LABEL().getText()); - abstractNTriplesQuadsListener.validateBlankNodeLabel(label); + String label = termDecoder.extractBlankNodeLabel(ctx.BLANK_NODE_LABEL().getText()); + termDecoder.validateBlankNodeLabel(label); return factory.createBNode(label); } if (ctx.literal() != null) { @@ -104,56 +102,19 @@ public Value extractObject(NTriplesParser.ObjectContext ctx) { */ protected Literal extractLiteral(NTriplesParser.LiteralContext ctx) { String rawText = ctx.STRING_LITERAL_QUOTE().getText(); - String label = abstractNTriplesQuadsListener.unescapeLiteral(rawText); + String label = termDecoder.unescapeLiteral(rawText); IRI datatype = null; String langTag = null; if (ctx.IRIREF() != null) { - String iri = abstractNTriplesQuadsListener.unescapeUri(abstractNTriplesQuadsListener.stripAngles(ctx.IRIREF().getText())); + String iri = termDecoder.unescapeUri(termDecoder.stripAngles(ctx.IRIREF().getText())); datatype = factory.createIRI(iri); } else if (ctx.LANGTAG() != null) { langTag = ctx.LANGTAG().getText().substring(1); } - return abstractNTriplesQuadsListener.createLiteral(label, datatype, langTag); + return termDecoder.createLiteral(label, datatype, langTag); } - /** - * Validates a blank node label according to RDF N-Triples specification. - * Blank node labels must not be empty and must not contain a colon. - * - * @param label The blank node label (without the "_:" prefix) - * @throws ParsingException if the label is invalid - * @deprecated Use helper.validateBlankNodeLabel instead - */ - @Deprecated(since = "4.5.0", forRemoval = false) - protected void validateBlankNodeLabel(String label) throws ParsingException { - abstractNTriplesQuadsListener.validateBlankNodeLabel(label); - } - - /** - * Unescapes common N-Triples literal escape sequences. - * - * @param literalText The raw literal string from ANTLR (including quotes) - * @return The unescaped literal string without surrounding quotes - * @deprecated Use helper.unescapeLiteral instead - */ - @Deprecated(since = "4.5.0", forRemoval = false) - public String unescapeLiteral(String literalText) { - return abstractNTriplesQuadsListener.unescapeLiteral(literalText); - } - - /** - * Unescapes common N-Triples URI escape sequences. - * - * @param uri The escaped URI string - * @return The unescaped URI string - * @deprecated Use helper.unescapeUri instead - */ - @Deprecated(since = "4.5.0", forRemoval = false) - protected String unescapeUri(String uri) { - return abstractNTriplesQuadsListener.unescapeUri(uri); - } } - diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesParser.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesParser.java index 37f7f1fbd..70e2afee3 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesParser.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesParser.java @@ -84,7 +84,7 @@ public void parse(Reader reader, String baseURI) throws ParsingException { antlrParser.setErrorHandler(new BailErrorStrategy()); antlrParser.addErrorListener(new NTriplesErrorListener()); - NTriplesListener listener = new NTriplesListener(getModel(), getValueFactory(), getConfig()); + NTriplesListener listener = new NTriplesListener(getModel(), getValueFactory()); LineRDFParser.parse(reader, lexer, antlrParser, antlrParser::ntriplesDoc, listener); } catch (ParseCancellationException pce) { diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/rdfa/RDFaParser.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/rdfa/RDFaParser.java index db5648063..f3e594eb2 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/rdfa/RDFaParser.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/rdfa/RDFaParser.java @@ -41,7 +41,7 @@ * This parser does NOT support vocabulary expansion *

*/ -public class RDFaParser extends AbstractRDFParser { +public final class RDFaParser extends AbstractRDFParser { private static final Logger logger = LoggerFactory.getLogger(RDFaParser.class); diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/AbstractTurtleTriGListener.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/AbstractTurtleTriGListener.java index 4551ac417..f4d1017d3 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/AbstractTurtleTriGListener.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/AbstractTurtleTriGListener.java @@ -51,7 +51,6 @@ protected AbstractTurtleTriGListener(Model model, ValueFactory factory, String b this.baseURI = baseURI; this.prefixHandler = new PrefixHandler(true); - initializeBasePrefix(); } /** @@ -68,12 +67,6 @@ public BNode scopedBlankNode(String label) { return blankNodeScope.computeIfAbsent(label, k -> factory.createBNode()); } - /** - * Registers the base URI as the empty prefix namespace. - */ - public void initializeBasePrefix() { - } - /** * Extracts IRI from angle brackets and processes Unicode escape sequences. * diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/AbstractNTriplesNQuadsListener.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/NTriplesQuadsTermDecoder.java similarity index 92% rename from src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/AbstractNTriplesNQuadsListener.java rename to src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/NTriplesQuadsTermDecoder.java index a226c7129..d9480fb22 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/AbstractNTriplesNQuadsListener.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/NTriplesQuadsTermDecoder.java @@ -2,32 +2,26 @@ 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.model.Model; import fr.inria.corese.core.next.data.api.factory.ValueFactory; -import fr.inria.corese.core.next.data.api.io.option.IOOptions; import fr.inria.corese.core.next.data.api.exception.ParsingException; /** - * Base class for N-Triples/N-Quads parsers providing common escape handling - * and validation logic. + * Decodes lexical RDF terms shared by the N-Triples and N-Quads listeners. + * + *

This helper handles escapes and term construction; it does not receive + * parse-tree events or retain the destination model and parser configuration.

*/ -public abstract class AbstractNTriplesNQuadsListener { +public final class NTriplesQuadsTermDecoder { - protected final Model model; - protected final ValueFactory factory; - protected final IOOptions options; + private final ValueFactory factory; /** - * Constructs a parser helper. + * Creates a decoder using the listener's RDF value factory. * - * @param model RDF model to populate * @param factory ValueFactory for creating RDF terms - * @param options IO configuration options */ - protected AbstractNTriplesNQuadsListener(Model model, ValueFactory factory, IOOptions options) { - this.model = model; + public NTriplesQuadsTermDecoder(ValueFactory factory) { this.factory = factory; - this.options = options; } /** diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/nquads/NQuadsSerializerOptions.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/nquads/NQuadsSerializerOptions.java index 2b7239827..e1e9101b6 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/nquads/NQuadsSerializerOptions.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/nquads/NQuadsSerializerOptions.java @@ -26,7 +26,7 @@ protected NQuadsSerializerOptions(Builder builder) { * Provides a fluent API for constructing {@code NQuadsConfig} instances with default values * specific to the N-Quads format. */ - public static class Builder extends AbstractNFamilyOptions.AbstractNFamilyBuilder { + public static final class Builder extends AbstractNFamilyOptions.AbstractNFamilyBuilder { /** * Default constructor initializes all options with their default values for N-Quads. */ diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/ntriples/NTriplesSerializerOptions.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/ntriples/NTriplesSerializerOptions.java index 59e246932..e0fc5d45c 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/ntriples/NTriplesSerializerOptions.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/ntriples/NTriplesSerializerOptions.java @@ -26,7 +26,7 @@ protected NTriplesSerializerOptions(Builder builder) { * Provides a fluent API for constructing {@code NTriplesConfig} instances with default values * specific to the N-Triples format. */ - public static class Builder extends AbstractNFamilyOptions.AbstractNFamilyBuilder { + public static final class Builder extends AbstractNFamilyOptions.AbstractNFamilyBuilder { /** * Default constructor initializes all options with their default values for N-Triples. */ diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/option/AbstractSerializerOptions.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/option/AbstractSerializerOptions.java index aed74d2d9..9fbe54f2c 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/option/AbstractSerializerOptions.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/option/AbstractSerializerOptions.java @@ -198,23 +198,23 @@ public abstract static class AbstractBuilder> { protected AbstractBuilder(IOOptions otherOptions) { if(otherOptions instanceof AbstractSerializerOptions abstractSerializerOptions) { - this.escapeUnicode(abstractSerializerOptions.escapeUnicode()); - this.trailingDot(abstractSerializerOptions.trailingDot()); - this.strictMode(abstractSerializerOptions.isStrictMode()); - this.validateURIs(abstractSerializerOptions.validateURIs()); - this.includeContext(abstractSerializerOptions.includeContext()); + this.escapeUnicode = abstractSerializerOptions.escapeUnicode(); + this.trailingDot = abstractSerializerOptions.trailingDot(); + this.strictMode = abstractSerializerOptions.isStrictMode(); + this.validateURIs = abstractSerializerOptions.validateURIs(); + this.includeContext = abstractSerializerOptions.includeContext(); } if(otherOptions instanceof BaseIRIOptions baseIRIOptions) { - this.baseIRI(baseIRIOptions.getBaseIRI()); + this.baseIRI = baseIRIOptions.getBaseIRI(); } if(otherOptions instanceof LineEndingOptions lineEndingOptions) { - this.lineEnding(lineEndingOptions.getLineEnding()); + this.lineEnding = Objects.requireNonNull(lineEndingOptions.getLineEnding()); } if(otherOptions instanceof BlankNodeIdGenerationOptions blankNodeIdGenerationOptions) { - this.stableBlankNodeIds(blankNodeIdGenerationOptions.stableBlankNodeIds()); + this.stableBlankNodeIds = blankNodeIdGenerationOptions.stableBlankNodeIds(); } if(otherOptions instanceof DatatypePolicyOptions datatypePolicyOptions) { - this.literalDatatypePolicy(datatypePolicyOptions.getLiteralDatatypePolicy()); + this.literalDatatypePolicy = Objects.requireNonNull(datatypePolicyOptions.getLiteralDatatypePolicy()); } } diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/option/AbstractTFamilyOptions.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/option/AbstractTFamilyOptions.java index 179922f7c..f5d8d86b2 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/option/AbstractTFamilyOptions.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/option/AbstractTFamilyOptions.java @@ -130,7 +130,7 @@ protected AbstractTFamilyOptions(AbstractTFamilyBuilder builder) { this.sortSubjects = builder.sortSubjects; this.sortPredicates = builder.sortPredicates; - if (this.escapeUnicode() && this.useMultilineLiterals) { + if (this.escapeUnicode && this.useMultilineLiterals) { throw new IllegalArgumentException("Cannot enable both escapeUnicode and useMultilineLiterals in Turtle TriG configs."); } } @@ -315,25 +315,25 @@ public abstract static class AbstractTFamilyBuilder { + public static final class Builder extends AbstractSerializerOptions.AbstractBuilder { protected boolean usePrefixes = true; protected boolean autoDeclarePrefixes = true; protected PrefixOrdering prefixOrdering = PrefixOrdering.ALPHABETICAL; diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/trig/TriGSerializerOptions.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/trig/TriGSerializerOptions.java index ca23d7fd7..c5b3f6c24 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/trig/TriGSerializerOptions.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/trig/TriGSerializerOptions.java @@ -28,7 +28,7 @@ protected TriGSerializerOptions(Builder builder) { * Provides a fluent API for constructing {@code TriGConfig} instances with default values * specific to the TriG format. */ - public static class Builder extends AbstractTFamilyOptions.AbstractTFamilyBuilder { + public static final class Builder extends AbstractTFamilyOptions.AbstractTFamilyBuilder { /** * Default constructor initializes all options with their default values for TriG. */ diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/turtle/TurtleSerializerOptions.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/turtle/TurtleSerializerOptions.java index 6930f974a..6b4141357 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/turtle/TurtleSerializerOptions.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/serializer/turtle/TurtleSerializerOptions.java @@ -29,7 +29,7 @@ protected TurtleSerializerOptions(Builder builder) { * Provides a fluent API for constructing {@code TurtleConfig} instances with default values * specific to the Turtle format. */ - public static class Builder extends AbstractTFamilyOptions.AbstractTFamilyBuilder { + public static final class Builder extends AbstractTFamilyOptions.AbstractTFamilyBuilder { /** * Default constructor initializes all options with their default values for Turtle. */ diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/model/ModelException.java b/src/main/java/fr/inria/corese/core/next/data/impl/model/ModelException.java index 50529ea71..ac188b7e1 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/model/ModelException.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/model/ModelException.java @@ -6,6 +6,9 @@ * Exception thrown when a Model operation fails. */ public class ModelException extends CoreseException { + @java.io.Serial + private static final long serialVersionUID = 1L; + /** * The operation that failed. diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/namespace/PrefixHandler.java b/src/main/java/fr/inria/corese/core/next/data/impl/namespace/PrefixHandler.java index 4facd9a9f..cc3ca00c4 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/namespace/PrefixHandler.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/namespace/PrefixHandler.java @@ -21,7 +21,7 @@ /** * Unified prefix handler for managing namespace prefix mappings across all RDF formats. */ -public class PrefixHandler implements PrefixMapping { +public final class PrefixHandler implements PrefixMapping { /** * Map of prefix to namespace URI. diff --git a/src/main/java/fr/inria/corese/core/next/data/spi/model/AbstractStatement.java b/src/main/java/fr/inria/corese/core/next/data/spi/model/AbstractStatement.java index 0e13b5491..4201e7415 100644 --- a/src/main/java/fr/inria/corese/core/next/data/spi/model/AbstractStatement.java +++ b/src/main/java/fr/inria/corese/core/next/data/spi/model/AbstractStatement.java @@ -17,6 +17,9 @@ */ public abstract class AbstractStatement implements Statement { + @java.io.Serial + private static final long serialVersionUID = 1L; + /** * Compares this statement to another object for equality. diff --git a/src/main/java/fr/inria/corese/core/next/data/spi/term/AbstractBNode.java b/src/main/java/fr/inria/corese/core/next/data/spi/term/AbstractBNode.java index 0ddf72c62..78cc100bb 100644 --- a/src/main/java/fr/inria/corese/core/next/data/spi/term/AbstractBNode.java +++ b/src/main/java/fr/inria/corese/core/next/data/spi/term/AbstractBNode.java @@ -7,6 +7,9 @@ * A blank node (BNode) ... */ public abstract class AbstractBNode implements BNode { + @java.io.Serial + private static final long serialVersionUID = 1L; + /** * Returns the string value of this blank node, which is its unique identifier. diff --git a/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractDuration.java b/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractDuration.java index 10ff2fa7e..01f3d9407 100644 --- a/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractDuration.java +++ b/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractDuration.java @@ -17,6 +17,9 @@ * Abstract class representing a duration literal in RDF. */ public abstract class AbstractDuration extends AbstractLiteral implements Comparable { + @java.io.Serial + private static final long serialVersionUID = 1L; + /** * Constructor for AbstractDuration. diff --git a/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractLiteral.java b/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractLiteral.java index 178204292..8b5278b0e 100644 --- a/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractLiteral.java +++ b/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractLiteral.java @@ -20,6 +20,9 @@ * Base class for all literals. All value accessors throw an exception by default. */ public abstract class AbstractLiteral implements Literal { + @java.io.Serial + private static final long serialVersionUID = 1L; + protected IRI datatype; diff --git a/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractNumber.java b/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractNumber.java index c04e66cc8..c503d9e45 100644 --- a/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractNumber.java +++ b/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractNumber.java @@ -6,6 +6,9 @@ * Abstract class representing a number literal in RDF. */ public abstract class AbstractNumber extends AbstractLiteral implements Comparable { + @java.io.Serial + private static final long serialVersionUID = 1L; + /** diff --git a/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractStringLiteral.java b/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractStringLiteral.java index 73e0e6a2e..683e7fe7b 100644 --- a/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractStringLiteral.java +++ b/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractStringLiteral.java @@ -10,6 +10,9 @@ @SuppressWarnings("java:S1210") public abstract class AbstractStringLiteral extends AbstractLiteral implements Comparable { + @java.io.Serial + private static final long serialVersionUID = 1L; + /** * Constructs an instance of AbstractStringLiteral with the specified datatype. * diff --git a/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractTemporalPointLiteral.java b/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractTemporalPointLiteral.java index dbae48126..1dccba928 100644 --- a/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractTemporalPointLiteral.java +++ b/src/main/java/fr/inria/corese/core/next/data/spi/term/literal/AbstractTemporalPointLiteral.java @@ -6,6 +6,9 @@ * Represents a temporal literal representing a point in time. Typically a date or a date and time. Implements the comparison between temporal literals. */ public abstract class AbstractTemporalPointLiteral extends AbstractLiteral implements Comparable { + @java.io.Serial + private static final long serialVersionUID = 1L; + /** * Constructor for AbstractTemporalPointLiteral. diff --git a/src/main/java/fr/inria/corese/core/next/query/api/Query.java b/src/main/java/fr/inria/corese/core/next/query/api/Query.java index 77982e326..04204a3cd 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/Query.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/Query.java @@ -2,6 +2,7 @@ import fr.inria.corese.core.next.data.api.term.Value; import fr.inria.corese.core.next.query.api.dataset.Dataset; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecution; import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; import fr.inria.corese.core.next.query.api.result.BindingSet; import fr.inria.corese.core.next.query.api.result.GraphQueryResult; @@ -50,6 +51,23 @@ public interface Query { */ T evaluate() throws QueryEvaluationException; + /** + * Executes this query once with planning and producer diagnostics enabled. + * Bindings, dataset, timeout and result ownership follow {@link #evaluate()}. + * This is an actual execution, not a dry-run explanation. No extra storage + * counts or scans are performed solely to populate the profile. Ordinary + * {@code evaluate()} calls do not allocate diagnostic collectors or wrappers. + * + *

For progressive SELECT results, {@code profile()} snapshots work performed + * so far. Consume or close the result before requesting final measurements. + * Lazy evaluation failures surface while advancing the result and release its + * resources. Prepared queries and diagnostic access are not thread-safe.

+ * + * @return result and access to immutable progress measurements + * @throws QueryEvaluationException if evaluation fails + */ + QueryExecution evaluateWithDiagnostics() throws QueryEvaluationException; + /** * Sets or replaces an initial variable binding. * diff --git a/src/main/java/fr/inria/corese/core/next/query/api/diagnostics/QueryExecution.java b/src/main/java/fr/inria/corese/core/next/query/api/diagnostics/QueryExecution.java new file mode 100644 index 000000000..098a62e39 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/api/diagnostics/QueryExecution.java @@ -0,0 +1,40 @@ +package fr.inria.corese.core.next.query.api.diagnostics; + +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Query result and access to immutable measurements of its progress. For a lazy + * SELECT, profile() snapshots the work performed so far; consume or close the + * result before requesting final measurements. Client idle time is excluded. + * This wrapper owns no resources. The caller must close tuple and graph results. + * Both result consumption and profile access belong to the evaluation thread. + * + * @param query result type + */ +public final class QueryExecution { + private final T result; + private final Supplier profiles; + + /** + * Creates an execution with a non-null result and profile source. + * + * @param result evaluated result owned by the caller + * @param profiles source of immutable progress measurements + * @throws NullPointerException if either argument is null + */ + public QueryExecution(T result, Supplier profiles) { + this.result = Objects.requireNonNull(result, "result"); + this.profiles = Objects.requireNonNull(profiles, "profiles"); + } + + /** @return the evaluated result, which must be closed when resource-bearing */ + public T result() { + return result; + } + + /** @return an immutable snapshot of the work completed so far */ + public QueryExecutionProfile profile() { + return profiles.get(); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/api/diagnostics/QueryExecutionProfile.java b/src/main/java/fr/inria/corese/core/next/query/api/diagnostics/QueryExecutionProfile.java new file mode 100644 index 000000000..c2d8017d1 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/api/diagnostics/QueryExecutionProfile.java @@ -0,0 +1,191 @@ +package fr.inria.corese.core.next.query.api.diagnostics; + +import java.util.List; +import java.util.Objects; + +/** + * Immutable measurements from an explicitly instrumented evaluation. Timings + * include instrumentation overhead and are wall-clock durations, not CPU times. + * Patterns follow the compiled traversal order within each operator scope; this + * is not a chronological trace of joins, branches or repeated invocations. + * + * @param compilationNanos time translating the parsed AST and capturing its compilation image; + * excludes parsing and initial query preparation + * @param planningNanos time completing the runtime query, including ordering, + * statistics and node indexing + * @param executionSetupNanos time creating independent execution containers from a + * compilation image; zero for direct non-prepared calls + * @param planReused whether a cached compilation image was reused without translation or planning + * @param evaluationNanos time executing the completed runtime query, including + * result mappings and cursor cleanup; excludes + * client idle time and graph template materialization + * @param patterns ordered logical patterns, followed by auxiliary runtime lookups + * such as path steps and dynamically compiled EXISTS patterns + * @param delivery actual result-delivery mode, fallback reason and first-row observation + */ +public record QueryExecutionProfile(long compilationNanos, long planningNanos, long executionSetupNanos, + long evaluationNanos, boolean planReused, List patterns, ResultDelivery delivery) { + + /** + * Copies the pattern list so the profile retains no mutable engine state. + * + * @throws IllegalArgumentException if a duration is negative + * @throws NullPointerException if the list, an element or the delivery observation is null + */ + public QueryExecutionProfile { + if (compilationNanos < 0 || planningNanos < 0 || executionSetupNanos < 0 || evaluationNanos < 0) { + throw new IllegalArgumentException("Durations must be non-negative"); + } + patterns = List.copyOf(patterns); + Objects.requireNonNull(delivery, "delivery"); + } + + /** How this invocation makes its result available to the caller. */ + public enum ExecutionMode { + /** Eligible SELECT rows are produced as the caller advances the cursor. */ + STREAMING, + /** Evaluation completes before the result is returned. */ + MATERIALIZED + } + + /** First guard that selected full evaluation; this is not an exhaustive query analysis. */ + public enum FallbackReason { + /** No fallback: a progressive cursor was actually opened. */ + NONE, + /** The invocation is ASK, CONSTRUCT or DESCRIBE rather than SELECT. */ + QUERY_FORM, + /** ORDER BY requires completing the input before delivering the selected rows. */ + ORDER_BY, + /** GROUP BY, aggregate expressions or HAVING require full evaluation. */ + AGGREGATION, + /** A property path uses the established complete evaluator. */ + PROPERTY_PATH, + /** A nested SELECT uses the established subquery evaluator. */ + SUBQUERY, + /** MINUS uses the established buffered evaluator. */ + MINUS, + /** SERVICE uses the established service evaluator. */ + SERVICE, + /** An OPTIONAL condition or right-hand scope requires full evaluation. */ + COMPLEX_OPTIONAL, + /** A scoped join retains its buffered strategy instead of repeated independent scans. */ + SCOPED_JOIN, + /** Repeated evaluation of an independent scope would risk quadratic work. */ + REPEATED_SCOPE, + /** Another operator or extension is not supported by the progressive cursor. */ + UNSUPPORTED_OPERATOR, + /** The backend cannot capture an independent stable read snapshot. */ + SNAPSHOT_UNAVAILABLE + } + + /** Meaning of the first-row timing for this invocation. */ + public enum FirstRowStatus { + /** First-row timing applies only to SELECT results. */ + NOT_APPLICABLE, + /** Neither availability nor successful exhaustion has been observed, including early close or failure. */ + UNOBSERVED, + /** At least one projected solution is ready, whether or not the caller consumed it. */ + AVAILABLE, + /** Evaluation completed successfully with no projected solution after OFFSET and LIMIT. */ + EMPTY + } + + /** + * Result strategy and first-row observation, independent of the query's row count. + * The timing accumulates work from entry into the profiled evaluation through + * the first successful availability observation. It includes compilation and + * planning, excludes initial preparation and client idle time, and is frozen + * once a row is ready. Materialized SELECT rows are ready before evaluate returns; + * streaming rows become ready in the first successful hasNext or next call. + * Closing without a successful observation does not establish that a query is empty. + * + * @param mode actual result-delivery mode for this invocation + * @param fallbackReason first materialization guard, or NONE for a progressive cursor + * @param firstRowStatus whether a SELECT row is ready, absent or not observed; not applicable to other query forms + * @param firstRowNanos accumulated active wall-clock nanoseconds to availability, or -1 unless AVAILABLE + */ + public record ResultDelivery(ExecutionMode mode, FallbackReason fallbackReason, + FirstRowStatus firstRowStatus, long firstRowNanos) { + /** + * Validates the strategy and observation without inventing a timing for an absent row. + * + * @throws NullPointerException if a classification is null + * @throws IllegalArgumentException if the strategy or first-row timing is inconsistent + */ + public ResultDelivery { + Objects.requireNonNull(mode, "mode"); + Objects.requireNonNull(fallbackReason, "fallbackReason"); + Objects.requireNonNull(firstRowStatus, "firstRowStatus"); + if ((mode == ExecutionMode.STREAMING) != (fallbackReason == FallbackReason.NONE)) { + throw new IllegalArgumentException("Only progressive delivery has no fallback reason"); + } + if ((firstRowStatus == FirstRowStatus.NOT_APPLICABLE) != (fallbackReason == FallbackReason.QUERY_FORM)) { + throw new IllegalArgumentException("First-row observations apply only to SELECT"); + } + if (firstRowNanos < -1 || (firstRowStatus == FirstRowStatus.AVAILABLE) != (firstRowNanos >= 0)) { + throw new IllegalArgumentException("Only an available first row has a non-negative timing"); + } + } + } + + /** + * A logical triple/path pattern or an auxiliary lookup compiled during execution (paths or EXISTS). + * Estimates describe unbound patterns at planning time, before runtime bindings + * and inter-graph deduplication. They are neither join-output predictions nor + * guarantees about the state later read by execution. No extra counts are + * requested for diagnostics: {@code -1} means the planner did not obtain one. + * + * @param location operator scope and child positions in the completed plan + * @param pattern textual pattern, including its GRAPH scope when applicable + * @param auxiliary whether this is an unplanned runtime lookup + * @param estimatedCardinality planner's count, or {@code -1} if unavailable + * @param iteratorOpenings successful openings of this producer iterable; + * these are not necessarily storage cursors + * @param candidateEdges edges returned by {@code next()} to the engine, + * after producer deduplication and before RDF matching; + * summed across invocations, not distinct solutions or + * internal index visits + */ + public record Pattern(String location, String pattern, boolean auxiliary, + long estimatedCardinality, long iteratorOpenings, long candidateEdges) { + /** + * Checks the pattern description and non-negative counters. + * + * @throws NullPointerException if a description is null + * @throws IllegalArgumentException if a count is outside its documented range + */ + public Pattern { + Objects.requireNonNull(location, "location"); + Objects.requireNonNull(pattern, "pattern"); + if (estimatedCardinality < -1 || iteratorOpenings < 0 || candidateEdges < 0) { + throw new IllegalArgumentException("Invalid pattern counters"); + } + } + } + + /** + * Formats the measurements without inspecting the repository or executing a query. + * + * @return a human-readable plan and its producer counters + */ + public String format() { + StringBuilder text = new StringBuilder("Compilation: ").append(compilationNanos) + .append(" ns; planning: ").append(planningNanos) + .append(" ns; execution setup: ").append(executionSetupNanos) + .append(" ns; reused plan: ").append(planReused).append("; evaluation: ").append(evaluationNanos).append(" ns\n"); + text.append("Delivery: ").append(delivery.mode()).append("; fallback: ").append(delivery.fallbackReason()) + .append("; first row: ").append(delivery.firstRowStatus()); + if (delivery.firstRowStatus() == FirstRowStatus.AVAILABLE) { + text.append(" (").append(delivery.firstRowNanos()).append(" ns)"); + } + text.append('\n'); + for (Pattern entry : patterns) { + text.append(entry.location()).append(" ").append(entry.pattern()) + .append(" | estimate=").append(entry.estimatedCardinality() < 0 + ? "unknown" : Long.toString(entry.estimatedCardinality())) + .append("; iterations=").append(entry.iteratorOpenings()) + .append("; candidates=").append(entry.candidateEdges()).append('\n'); + } + return text.toString(); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/api/diagnostics/package-info.java b/src/main/java/fr/inria/corese/core/next/query/api/diagnostics/package-info.java new file mode 100644 index 000000000..2e832dc9a --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/api/diagnostics/package-info.java @@ -0,0 +1,2 @@ +/** Opt-in execution diagnostics, independent of the mutable engine and storage state. */ +package fr.inria.corese.core.next.query.api.diagnostics; diff --git a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryEvaluationException.java b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryEvaluationException.java index b52f1ebfc..78ded8360 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryEvaluationException.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryEvaluationException.java @@ -5,6 +5,9 @@ */ @SuppressWarnings("java:S110") public class QueryEvaluationException extends QueryException { + @java.io.Serial + private static final long serialVersionUID = 1L; + /** * Constructs a QueryEvaluationException with a detail message. diff --git a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryException.java b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryException.java index e93e30c68..a982bc00c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryException.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryException.java @@ -7,6 +7,9 @@ * Base exception for all query-related errors in the Corese query API. */ public class QueryException extends CoreseException { + @java.io.Serial + private static final long serialVersionUID = 1L; + /** * Constructs a new QueryException with the specified detail message. diff --git a/src/main/java/fr/inria/corese/core/next/query/api/exception/QuerySyntaxException.java b/src/main/java/fr/inria/corese/core/next/query/api/exception/QuerySyntaxException.java index 0cc4162c6..50916d9b9 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/exception/QuerySyntaxException.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/exception/QuerySyntaxException.java @@ -5,6 +5,9 @@ */ @SuppressWarnings("java:S110") public class QuerySyntaxException extends QueryException { + @java.io.Serial + private static final long serialVersionUID = 1L; + private final int line; private final int column; diff --git a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryTimeoutException.java b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryTimeoutException.java index d5d75c31a..4896937c5 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryTimeoutException.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryTimeoutException.java @@ -5,6 +5,9 @@ */ @SuppressWarnings("java:S110") public class QueryTimeoutException extends QueryEvaluationException { + @java.io.Serial + private static final long serialVersionUID = 1L; + /** * Constructs a QueryTimeoutException with the timeout value that was exceeded. diff --git a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryTypeErrorException.java b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryTypeErrorException.java index aa8daad11..9ed657382 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryTypeErrorException.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryTypeErrorException.java @@ -7,6 +7,9 @@ */ @SuppressWarnings("java:S110") // Retains the public query exception hierarchy and distinguishes SPARQL type errors. public final class QueryTypeErrorException extends QueryEvaluationException { + @java.io.Serial + private static final long serialVersionUID = 1L; + public QueryTypeErrorException(String message) { super(message); } diff --git a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryValidationException.java b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryValidationException.java index de20ae0d9..c44bf79ca 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryValidationException.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/exception/QueryValidationException.java @@ -5,6 +5,9 @@ */ @SuppressWarnings("java:S110") public class QueryValidationException extends QueryException { + @java.io.Serial + private static final long serialVersionUID = 1L; + /** * Constructs a QueryValidationException with a detail message. diff --git a/src/main/java/fr/inria/corese/core/next/query/api/exception/RepositoryException.java b/src/main/java/fr/inria/corese/core/next/query/api/exception/RepositoryException.java index 30296af26..3b7ebd7f4 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/exception/RepositoryException.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/exception/RepositoryException.java @@ -9,6 +9,9 @@ */ @SuppressWarnings("java:S110") public class RepositoryException extends QueryException { + @java.io.Serial + private static final long serialVersionUID = 1L; + /** * Constructs a RepositoryException with a detail message. diff --git a/src/main/java/fr/inria/corese/core/next/query/api/exception/UnsupportedQueryFeatureException.java b/src/main/java/fr/inria/corese/core/next/query/api/exception/UnsupportedQueryFeatureException.java index 939dd14b9..399fa7968 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/exception/UnsupportedQueryFeatureException.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/exception/UnsupportedQueryFeatureException.java @@ -5,6 +5,9 @@ */ @SuppressWarnings("java:S110") public class UnsupportedQueryFeatureException extends QueryException { + @java.io.Serial + private static final long serialVersionUID = 1L; + /** * Constructs an UnsupportedQueryFeatureException with a detail message. diff --git a/src/main/java/fr/inria/corese/core/next/query/api/result/TupleQueryResult.java b/src/main/java/fr/inria/corese/core/next/query/api/result/TupleQueryResult.java index e01f3012e..8d29faf40 100644 --- a/src/main/java/fr/inria/corese/core/next/query/api/result/TupleQueryResult.java +++ b/src/main/java/fr/inria/corese/core/next/query/api/result/TupleQueryResult.java @@ -16,7 +16,17 @@ * corresponding to the projection variables of the query. *

* - *

Implementations are not required to be thread-safe.

+ *

Results may evaluate lazily during {@link #hasNext()} and {@link #next()}. + * Native progressive results must be consumed on the thread that evaluated the + * query. They retain a stable read snapshot, including the transaction's visible + * uncommitted data, until exhaustion, failure or closure. Always use + * try-with-resources, especially for partial consumption: closing a stream early + * releases the result, but a stream terminal operation alone does not close it. + * Closing a result never commits or rolls back its source transaction.

+ * + *

Implementations are not required to be thread-safe. Native cursors permit + * cross-thread close for cancellation. Their query timeout spans preparation, + * traversal and client idle time; idle expiration never interrupts client code.

* * @see TupleQuery * @see TupleQuery#evaluate() @@ -37,6 +47,8 @@ public interface TupleQueryResult extends Closeable, Iterable { * * @return {@code true} if another binding set is available, {@code false} otherwise * @throws IllegalStateException if this result has been closed + * @throws fr.inria.corese.core.next.query.api.exception.QueryEvaluationException + * if lazy evaluation fails, is interrupted or reaches its timeout */ boolean hasNext(); @@ -47,6 +59,8 @@ public interface TupleQueryResult extends Closeable, Iterable { * @throws NoSuchElementException if no more solutions are available * (i.e., {@link #hasNext()} would return {@code false}) * @throws IllegalStateException if this result has been closed + * @throws fr.inria.corese.core.next.query.api.exception.QueryEvaluationException + * if lazy evaluation fails, is interrupted or reaches its timeout */ BindingSet next(); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/DiagnosticProducer.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/DiagnosticProducer.java new file mode 100644 index 000000000..e830a5264 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/DiagnosticProducer.java @@ -0,0 +1,74 @@ +package fr.inria.corese.core.next.query.impl.engine.diagnostics; + +import fr.inria.corese.core.next.query.impl.engine.model.Edge; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; +import fr.inria.corese.core.next.query.impl.engine.spi.Environment; +import fr.inria.corese.core.next.query.impl.engine.storage.StorageManagerProducer; +import fr.inria.corese.core.next.storage.api.StorageManager; + +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +/** + * Opt-in producer decorator. The ordinary producer and its cursors have no + * diagnostic fields, counter updates or per-statement branches. The underlying + * producer retains cursor ownership, cancellation and snapshot release. + */ +public final class DiagnosticProducer extends StorageManagerProducer { + private final ExecutionDiagnostics diagnostics; + + /** + * Creates a request-scoped instrumented producer. + * + * @param storage session-visible storage + * @param diagnostics collector confined to the evaluation thread + * @throws NullPointerException if either argument is null + */ + public DiagnosticProducer(StorageManager storage, ExecutionDiagnostics diagnostics) { + super(storage); + this.diagnostics = Objects.requireNonNull(diagnostics, "diagnostics"); + } + + @Override + public long estimateCardinality(Query query, Node graphNode, Edge edge) { + long count = super.estimateCardinality(query, graphNode, edge); + diagnostics.estimated(edge, count); + return count; + } + + @Override + public Iterable getEdges(Node graphNode, List from, Edge queryEdge, Environment environment) { + Iterable delegate = super.getEdges(graphNode, from, queryEdge, environment); + return new DiagnosticEdges(delegate, diagnostics.executing(queryEdge, graphNode)); + } + + @Override + public void closeEdges(Iterable edges) { + super.closeEdges(edges instanceof DiagnosticEdges measured ? measured.delegate : edges); + } + + /** Wraps only explicitly profiled traversals and preserves repeatable iteration. */ + private record DiagnosticEdges(Iterable delegate, + ExecutionDiagnostics.PatternCounters counters) implements Iterable { + @Override + public Iterator iterator() { + Iterator source = delegate.iterator(); + counters.opened(); + return new Iterator<>() { + @Override + public boolean hasNext() { + return source.hasNext(); + } + + @Override + public Edge next() { + Edge edge = source.next(); + counters.returned(); + return edge; + } + }; + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/ExecutionDiagnostics.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/ExecutionDiagnostics.java new file mode 100644 index 000000000..9dc53a5d0 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/ExecutionDiagnostics.java @@ -0,0 +1,268 @@ +package fr.inria.corese.core.next.query.impl.engine.diagnostics; + +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.ExecutionMode; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.FallbackReason; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.FirstRowStatus; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.ResultDelivery; +import fr.inria.corese.core.next.data.api.term.Value; +import fr.inria.corese.core.next.data.impl.io.serializer.rdfc10.StatementUtils; +import fr.inria.corese.core.next.query.impl.engine.model.Edge; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.path.PropertyPathEdge; +import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.Set; +import java.util.Objects; +import java.util.function.LongSupplier; + +/** + * Collector confined to the evaluation thread. Timeout cancellation closes the + * producer but never reads or updates these counters. Only immutable strings, + * counts and copied lists escape through {@link #snapshot()}. + */ +public final class ExecutionDiagnostics { + private final LongSupplier nanoTime; + private final IdentityHashMap counters = new IdentityHashMap<>(); + private final Map auxiliary = new HashMap<>(); + private final List ordered = new ArrayList<>(); + private final Set expressions = Collections.newSetFromMap(new IdentityHashMap<>()); + private long compilationNanos; + private long planningNanos; + private long evaluationNanos; + private long executionSetupNanos; + private boolean planReused; + private ExecutionMode mode = ExecutionMode.MATERIALIZED; + private FallbackReason fallbackReason = FallbackReason.QUERY_FORM; + private FirstRowStatus firstRowStatus = FirstRowStatus.NOT_APPLICABLE; + private long firstRowNanos = -1; + private long activeNanos; + private long workStarted; + private int workDepth; + + /** Creates an evaluation-thread collector using the monotonic system clock. */ + public ExecutionDiagnostics() { + this(System::nanoTime); + } + + /** Creates a collector with a monotonic clock, allowing deterministic timing verification. */ + ExecutionDiagnostics(LongSupplier nanoTime) { + this.nanoTime = Objects.requireNonNull(nanoTime, "nanoTime"); + } + + /** Starts a segment of query work; nested segments do not double-count time. */ + public void beginWork() { + if (workDepth++ == 0) workStarted = nanoTime.getAsLong(); + } + + /** + * Ends a query-work segment, excluding subsequent client idle time. + * + * @return elapsed nanoseconds for an outer segment, or zero for a nested segment + * @throws IllegalStateException if no query-work segment is active + */ + public long endWork() { + if (workDepth == 0) throw new IllegalStateException("No diagnostic work segment is active"); + if (--workDepth != 0) return 0; + long elapsed = nanoTime.getAsLong() - workStarted; + activeNanos += elapsed; + return elapsed; + } + + /** + * Records the actual SELECT strategy after checking algebra and snapshot availability. + * + * @param reason NONE for streaming, otherwise the guard selecting full evaluation + * @throws IllegalArgumentException if the reason describes a non-SELECT query form + */ + public void selectDelivery(FallbackReason reason) { + Objects.requireNonNull(reason, "reason"); + if (reason == FallbackReason.QUERY_FORM) throw new IllegalArgumentException("Expected a SELECT strategy"); + fallbackReason = reason; + mode = reason == FallbackReason.NONE ? ExecutionMode.STREAMING : ExecutionMode.MATERIALIZED; + firstRowStatus = FirstRowStatus.UNOBSERVED; + firstRowNanos = -1; + } + + /** + * Records the first successful SELECT availability or exhaustion observation. + * Later observations cannot overwrite the first timing or turn an early close into emptiness. + * + * @param available whether a projected solution is ready after OFFSET and LIMIT + */ + public void firstRow(boolean available) { + if (firstRowStatus != FirstRowStatus.UNOBSERVED) return; + firstRowStatus = available ? FirstRowStatus.AVAILABLE : FirstRowStatus.EMPTY; + if (available) { + firstRowNanos = activeNanos + (workDepth == 0 ? 0 : nanoTime.getAsLong() - workStarted); + } + } + + /** + * Records runtime-query translation duration. + * + * @param nanos elapsed wall-clock nanoseconds + */ + public void compilationTime(long nanos) { + compilationNanos += nanos; + } + + /** + * Records completion duration and captures the resulting operator order. + * + * @param query completed runtime query + * @param nanos elapsed completion nanoseconds + */ + public void planned(Query query, long nanos) { + planningNanos += nanos; + visit(query, "query", null); + } + + /** + * Records evaluation duration, excluding completion and plan capture. + * + * @param nanos elapsed evaluation nanoseconds + */ + public void evaluationTime(long nanos) { + evaluationNanos += nanos; + } + + /** Records structural execution-frame creation, independently of compilation and planning. + * @param nanos elapsed frame creation nanoseconds + */ + public void executionSetupTime(long nanos) { + executionSetupNanos += nanos; + } + + /** Records whether this evaluation used an already cached compilation image. + * @param reused whether compilation and planning were reused + */ + public void reusedPlan(boolean reused) { + planReused = reused; + } + + /** + * Remembers an estimate already requested by the planner. + * + * @param edge identity of the compiled pattern + * @param count estimate or {@code -1} + */ + public void estimated(Edge edge, long count) { + counters.computeIfAbsent(edge, ignored -> new PatternCounters()).estimate = count; + } + + /** + * Finds the counters for an executed edge, adding auxiliary runtime lookups when needed. + * + * @param edge compiled edge or dynamically compiled pattern + * @param graph effective graph supplied to the producer + * @return mutable counters confined to the evaluation thread + */ + PatternCounters executing(Edge edge, Node graph) { + PatternCounters entry = counters.get(edge); + if (entry != null && entry.location != null) { + return entry; + } + // Runtime EXISTS/path operators may be recreated for every binding. + // Aggregate their identical descriptions without retaining each transient edge. + String text = pattern(edge, graph); + PatternCounters runtime = auxiliary.get(text); + long estimate = entry == null ? -1 : entry.estimate; + if (runtime == null) { + runtime = new PatternCounters(); + runtime.estimate = estimate; + describe(runtime, text, "runtime/auxiliary", true); + auxiliary.put(text, runtime); + } else if (runtime.estimate != estimate) { + // Identical auxiliary descriptions may originate in different scopes. + runtime.estimate = -1; + } + return runtime; + } + + private void visit(Exp expression, String location, Node graph) { + if (!expressions.add(expression)) { + return; + } + Node scope = expression.isGraph() ? expression.getGraphName() : graph; + String here = location + "/" + expression.type(); + if (expression.getEdge() != null) { + Edge edge = expression.getEdge(); + PatternCounters entry = counters.computeIfAbsent(edge, ignored -> new PatternCounters()); + if (entry.location == null) { + describe(entry, pattern(edge, scope), here, false); + } + } + for (int i = 0; i < expression.size(); i++) { + visit(expression.get(i), here + "[" + i + "]", scope); + } + // EXISTS expressions compile their actual operators during evaluation. + // Do not call Expr.getPattern(): it would create a different, unexecuted tree. + } + + private static String pattern(Edge edge, Node graph) { + String predicate = edge instanceof PropertyPathEdge path + ? path.path().toString() : node(edge.getProperty()); + String triple = node(edge.getNode(0)) + " " + predicate + " " + node(edge.getNode(1)); + return graph == null ? triple : "GRAPH " + node(graph) + " { " + triple + " }"; + } + + private static String node(Node node) { + if (node == null) { + return "*"; + } + if (!node.isVariable() && node.getDatatypeValue() instanceof Value value) { + return StatementUtils.serializeForComparison(value); + } + return node.toString(); + } + + private void describe(PatternCounters entry, String pattern, String location, boolean auxiliaryLookup) { + entry.pattern = pattern; + entry.location = location; + entry.auxiliary = auxiliaryLookup; + ordered.add(entry); + } + + /** + * Copies measurements without retaining compiled nodes, bindings or storage snapshots. + * + * @return immutable execution profile + */ + public QueryExecutionProfile snapshot() { + return new QueryExecutionProfile(compilationNanos, planningNanos, executionSetupNanos, evaluationNanos, planReused, + ordered.stream().map(PatternCounters::snapshot).toList(), + new ResultDelivery(mode, fallbackReason, firstRowStatus, firstRowNanos)); + } + + /** Mutable producer counters, never shared with the caller or watchdog thread. */ + static final class PatternCounters { + private String location; + private String pattern; + private boolean auxiliary; + private long estimate = -1; + private long openings; + private long candidates; + + /** Records a successful iterable opening. */ + void opened() { + openings++; + } + + /** Records one candidate returned by the producer iterator. */ + void returned() { + candidates++; + } + + private QueryExecutionProfile.Pattern snapshot() { + return new QueryExecutionProfile.Pattern(location, pattern, auxiliary, estimate, openings, candidates); + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/package-info.java new file mode 100644 index 000000000..b8c3105e8 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/package-info.java @@ -0,0 +1,2 @@ +/** Request-scoped instrumentation used only by explicitly profiled evaluations. */ +package fr.inria.corese.core.next.query.impl.engine.diagnostics; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/ApproximateSearchEnv.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/ApproximateSearchEnv.java deleted file mode 100644 index bf5a34ddf..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/ApproximateSearchEnv.java +++ /dev/null @@ -1,104 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.eval; - -import fr.inria.corese.core.next.query.impl.engine.model.Expr; -import fr.inria.corese.core.next.query.impl.engine.model.Node; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -/** - * Data structure: Key -> (node -> Value) - * Key: (?variable, ) Value: (node, SIMILARITY, algs) - * - * @author Fuqi Song, Wimmics Inria I3S - */ -public class ApproximateSearchEnv { - - private final Map> all; - - public ApproximateSearchEnv() { - this.all = new HashMap<>(); - } - - public Double getSimilarity(Expr variable, Node node) { - Key key = new Key(variable); - Value r = this.get(key, node); - return (r == null) ? null : r.getSimilarity(); - } - - private Value get(Key key, Node node) { - Map candidates = all.get(key); - return candidates == null ? null : candidates.get(node); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder("Approximate search\n"); - for (Map.Entry> entrySet : all.entrySet()) { - Key key = entrySet.getKey(); - Map value = entrySet.getValue(); - sb.append(key).append("\n"); - for (Value v : value.values()) { - sb.append("\t").append(v.toString()).append("\n"); - } - } - return sb.toString(); - } - - static class Key { - - private final Expr variable; - private Node uri; - - public Key(Expr variable) { - this.variable = variable; - } - - public Expr getVar() { - return variable; - } - - @Override - public int hashCode() { - return 5; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - Key other = (Key) obj; - return Objects.equals(this.variable, other.variable); - } - - @Override - public String toString() { - return "Key{" + "variable=" + variable + ", uri=" + uri + '}'; - } - } - - static class Value { - - private final Node node; - private static final double SIMILARITY = -1; - private final String algorithms; - - public Value(Node node, String algorithms) { - this.node = node; - this.algorithms = algorithms; - } - - public double getSimilarity() { - return SIMILARITY; - } - - @Override - public String toString() { - return "[" + node + ", " + SIMILARITY + ", " + algorithms + "]"; - } - } -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/CompleteSPARQL.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/CompleteSPARQL.java deleted file mode 100644 index 9630a9c18..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/CompleteSPARQL.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * To change this template, choose Tools | Templates - * and open the template in the editor. - */ - -package fr.inria.corese.core.next.query.impl.engine.eval; - -import fr.inria.corese.core.next.query.api.exception.QueryTypeErrorException; -import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; -import fr.inria.corese.core.next.query.impl.engine.pattern.Query; -import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; -import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; - -import fr.inria.corese.core.next.query.impl.engine.model.Filter; -import fr.inria.corese.core.next.query.impl.engine.model.Node; -import fr.inria.corese.core.next.query.impl.engine.spi.Producer; -import fr.inria.corese.core.next.data.api.model.DatatypeValue; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -/** - * Compute select expression, - * Compute value of group by, order by - * - * @author Olivier Corby, Wimmics INRIA I3S, 2016 - */ -public class CompleteSPARQL { - private static final Logger logger = LoggerFactory.getLogger(CompleteSPARQL.class); - - Eval eval; - Query query; - - CompleteSPARQL(Query q, Eval e) { - this.eval = e; - this.query = q; - } - - /** - * Complete: - * select (exp as var) - * group by, order by - * - */ - void complete(Producer p, Mappings map) { - selectExpression(query, p, map); - distinct(query, map); - orderGroup(query, p, map); - } - - void distinct(Query q, Mappings map) { - if (q.isAggregate() || !map.isDistinct()) { - return; - } - - new ArrayList<>(map.getList()).forEach(m -> { - map.getList().clear(); - map.submit(m); - }); - } - void selectExpression(Query q, Producer p, Mappings map) { - if (query.isSelectExpression()) { - HashMap bnode = new HashMap<>(); - for (Mapping m : map) { - bnode.clear(); - m.setMap(bnode); - m.setQuery(q); - Mapping res = selectExpression(q, p, m); - if (res == null) { - logger.warn("Select: exp != var value: {}", m); - } - } - } - } - - Mapping selectExpression(Query q, Producer p, Mapping m) { - ArrayList ql = new ArrayList<>(); - ArrayList tl = new ArrayList<>(); - - for (Exp e : q.getSelectFun()) { - if (!completeProjection(e, p, m, ql, tl)) { - return null; - } - } - - if (!ql.isEmpty()) { - m.complete(ql, tl); - } - - return m; - } - - private boolean completeProjection(Exp expression, Producer producer, Mapping mapping, - List queryNodes, List targetNodes) { - Filter filter = expression.getFilter(); - if (filter == null) { - return true; - } - Node queryNode = expression.getNode(); - Node current = mapping.getNodeValue(queryNode); - if (expression.isAggregate()) { - // Aggregates are evaluated later; reserve their output slot now. - if (current == null) { - queryNodes.add(queryNode); - targetNodes.add(null); - } - return true; - } - Node result; - try { - result = eval.eval(null, filter, mapping, producer); - } catch (QueryTypeErrorException error) { - return true; - } - if (result == null) { - return true; - } - if (current != null) { - return current.equals(result); - } - queryNodes.add(queryNode); - targetNodes.add(result); - mapping.setNodeValue(queryNode, result); - return true; - } - - void orderGroup(Query q, Producer p, Mappings map) { - for (Mapping m : map) { - Node[] snode = new Node[q.getOrderBy().size()]; - Node[] gnode = new Node[q.getGroupBy().size()]; - orderGroup(q.getOrderBy(), snode, p, m); - orderGroup(q.getGroupBy(), gnode, p, m); - m.setOrderBy(snode); - m.setGroupBy(gnode); - } - } - - void orderGroup(List lExp, Node[] nodes, Producer p, Mapping m) { - int n = 0; - for (Exp e : lExp) { - nodes[n] = resolveOrderGroupNode(e, p, m); - n++; - } - } - - private Node resolveOrderGroupNode(Exp e, Producer p, Mapping m) { - Node qNode = e.getNode(); - Node node = qNode != null ? m.getNodeValue(qNode) : null; - if (node == null) { - node = evaluateOrderGroupFilter(e, p, m); - } - if (qNode != null && node != null) { - m.setNode(qNode, node); - } - return node; - } - - private Node evaluateOrderGroupFilter(Exp e, Producer p, Mapping m) { - Filter f = e.getFilter(); - if (f != null && !e.isAggregate()) { - try { - return eval.eval(null, f, m, p); - } catch (QueryTypeErrorException error) { - return null; - } - } - return null; - } - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EnvironmentImpl.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EnvironmentImpl.java index 23081f33d..efffa461c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EnvironmentImpl.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EnvironmentImpl.java @@ -19,97 +19,97 @@ public class EnvironmentImpl implements Environment { protected static final String NOT_SUPPORTED_YET = "Not supported yet."; - protected Query query; + protected Query query; - public EnvironmentImpl(){ - // The base environment starts without a query or bindings. - } + public EnvironmentImpl(){ + // The base environment starts without a query or bindings. + } @Override - public int count() { - return 0; - } + public int count() { + return 0; + } @Override - public Node getNode(Expr variable){ - return null; - } + public Node getNode(Expr variable){ + return null; + } @Override public int size() { return 0; } - @Override - public Node getNode(String label) { - return null; - } + @Override + public Node getNode(String label) { + return null; + } - @Override - public Node getNode(Node node) { - return null; - } + @Override + public Node getNode(Node node) { + return null; + } - @Override - public Node getQueryNode(int n) { - return null; - } + @Override + public Node getQueryNode(int n) { + return null; + } - @Override - public Node getQueryNode(String label) { - return null; - } + @Override + public Node getQueryNode(String label) { + return null; + } - @Override - public boolean isBound(Node node) { - return false; - } + @Override + public boolean isBound(Node node) { + return false; + } - @Override - public int pathLength(Node node) { - return 0; - } + @Override + public int pathLength(Node node) { + return 0; + } - @Override - public Query getQuery() { - return query; - } + @Override + public Query getQuery() { + return query; + } - @Override - public KgramEventDispatcher getEventManager() { - return null; - } + @Override + public KgramEventDispatcher getEventManager() { + return null; + } - @Override - public Node getGraphNode() { - return null; - } + @Override + public Node getGraphNode() { + return null; + } - @Override - public void setObject(Object o) { - // The base environment does not retain an attached object. - } + @Override + public void setObject(Object o) { + // The base environment does not retain an attached object. + } - @Override - public Object getObject() { - return null; - } + @Override + public Object getObject() { + return null; + } - @Override - public void setExp(Exp exp) { - // Expression storage is supplied by concrete execution environments. - } + @Override + public void setExp(Exp exp) { + // Expression storage is supplied by concrete execution environments. + } - @Override - public Exp getExp() { - return null; - } + @Override + public Exp getExp() { + return null; + } - public Map getMap() { - return Map.of(); - } + public Map getMap() { + return Map.of(); + } @Override public Edge[] getEdges() { @@ -152,11 +152,6 @@ public boolean hasBind() { throw new UnsupportedOperationException(NOT_SUPPORTED_YET); } - @Override - public ApproximateSearchEnv getAppxSearchEnv() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - @Override public void setBind(BindingContext b) { throw new UnsupportedOperationException(NOT_SUPPORTED_YET); 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 ac3742fae..92ac8e051 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 @@ -18,10 +18,8 @@ import fr.inria.corese.core.next.query.impl.engine.spi.Environment; import fr.inria.corese.core.next.query.impl.engine.spi.Evaluator; import fr.inria.corese.core.next.query.impl.engine.spi.Matcher; -import fr.inria.corese.core.next.query.impl.engine.spi.Plugin; import fr.inria.corese.core.next.query.impl.engine.spi.Producer; import fr.inria.corese.core.next.query.impl.engine.spi.Provider; -import fr.inria.corese.core.next.query.impl.engine.spi.SPARQLEngine; import fr.inria.corese.core.next.query.api.exception.QueryTypeErrorException; import fr.inria.corese.core.next.data.Values; @@ -29,7 +27,6 @@ import fr.inria.corese.core.next.query.impl.engine.event.EventImpl; import fr.inria.corese.core.next.query.impl.engine.event.KgramEventDispatcher; import fr.inria.corese.core.next.query.impl.engine.event.ResultListener; -import fr.inria.corese.core.next.query.impl.engine.path.PathFinder; 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; @@ -42,29 +39,29 @@ import static fr.inria.corese.core.next.query.impl.engine.model.ExprType.UNNEST; /** - * KGRAM Knowledge Graph Abstract Machine Compute graph homomorphism and - * (extended) SPARQL Use: a Stack of expression Exp a Memory for Node/Edge - * bindings an abstract Producer of candidate Node/Edge an abstract Evaluator of - * Filter an abstract Matcher of Node/Edge - *

- * - path statement is an EDGE with a boolean isPath this edge needs an Edge - * Node (a property variable) - *

- *

- * Note: future optimization: query ordering, search by dichotomy (in a cache) + * Backtracking evaluator for compiled SPARQL operator graphs. + * + *

{@link Stack} holds the pending operators, {@link Memory} owns their node + * and edge bindings, and {@link Producer} enumerates candidates. Expression + * evaluation and RDF-term compatibility use the evaluator and matcher contracts. + * Producer traversals are closed when exhausted or abandoned.

+ * + *

Instances and their mutable binding state are confined to the evaluation thread. + * A timeout watchdog may invoke {@link #finish()} concurrently; cancellation is + * published through volatile flags in this evaluator and its operator helpers.

* * @author Olivier Corby, Edelweiss, INRIA 2010 */ -public final class Eval implements ExpType, Plugin { +public final class Eval implements ExpType { static final int STOP = -2; - private static final int DISPLAY_RESULT_LIMIT = 10; static final Logger logger = LoggerFactory.getLogger(Eval.class); - // draft test: when edge() has Mappings map parameter, push clause values(map) + + // Propagate candidate mappings when evaluating edges. private boolean pushEdgeMappings = true; - // draft test: graph() has Mappings map parameter and eval body with map parameter + // Restrict graph evaluation using incoming solution mappings. private boolean parameterGraphMappings = true; - // draft test: union() has Mappings map parameter and eval branch with map parameter + // Supply incoming mappings independently to both UNION branches. private boolean parameterUnionMappings = true; KgramEventDispatcher manager; @@ -75,9 +72,6 @@ public final class Eval implements ExpType, Plugin { // Filter evaluator Evaluator evaluator; Matcher match; - List lPathFinder; - // Processing EXTERN expressions - Plugin plugin; // Stacks for binding edges and nodes Memory memory; Query query; @@ -85,10 +79,8 @@ public final class Eval implements ExpType, Plugin { Mappings results; // initial results to be completed Mappings initialResults; - EvalSPARQL evalSparql; - CompleteSPARQL completeSparql; List empty = new ArrayList<>(0); - EvalGraph evalGraphNew; + EvalGraph graphEvaluator; EvalJoin join; EvalOptional optional; int // count number of eval() calls @@ -106,7 +98,6 @@ public final class Eval implements ExpType, Plugin { boolean hasFilter = false; private ResultListener listener; private ProcessVisitor visitor; - private SPARQLEngine sparqlEngine; // Edge and Node producer private Producer producer; private Stack current; @@ -115,12 +106,9 @@ public final class Eval implements ExpType, Plugin { private boolean hasCandidate = false; private boolean hasStatement = false; private boolean hasProduce = false; - private boolean stopped = false; + private volatile boolean stopped = false; private boolean joinMappings = true; - public Eval() { - } - /** * @param p edge and node producer * @param e filter evaluator, given an environment (access to variable @@ -131,11 +119,9 @@ public Eval(Producer p, Evaluator e, Matcher m) { saveProducer = producer; evaluator = e; match = m; - plugin = this; - lPathFinder = new ArrayList<>(); setVisitor(new ProcessVisitorDefault()); e.setKGRAM(this); - evalGraphNew = new EvalGraph(this); + graphEvaluator = new EvalGraph(this); join = new EvalJoin(this); optional = new EvalOptional(this); @@ -194,15 +180,15 @@ public void set(Evaluator e) { /** * Main eval function */ - public Mappings query(Query q) throws SparqlException { + public Mappings query(Query q) { return query(null, q, null); } - public Mappings query(Node graphNode, Query q, Mapping m) throws SparqlException { + public Mappings query(Node graphNode, Query q, Mapping m) { return queryBasic(graphNode, q, m); } - Mappings queryBasic(Node graphNode, Query q, Mapping m) throws SparqlException { + Mappings queryBasic(Node graphNode, Query q, Mapping m) { if (hasEvent) { send(Event.BEGIN, q); } @@ -221,11 +207,7 @@ Mappings queryBasic(Node graphNode, Query q, Mapping m) throws SparqlException { } return map; } finally { - try { - getProducer().finish(q); - } finally { - clean(); - } + getProducer().finish(q); } } @@ -243,9 +225,7 @@ void share(Mapping m) { } } - // store ProcessVisitor into Bind for future sharing by - // Transformer and Interpreter exist - // use case: metadata @share + // Propagate a shareable visitor to nested evaluations through their bindings. void share(ProcessVisitor vis) { if (vis.isShareable() && getBind().getVisitor() == null) { getBind().setVisitor(vis); @@ -255,38 +235,31 @@ void share(ProcessVisitor vis) { /** * Mapping m is binding parameter, possibly null - * a) from template call with parameter: st:call-template(st:name, ?x, ?y) - * b) from query exec call with parameter: exec.query(q, m) + * Supplied bindings are applied before the main query or subquery is evaluated. * Mappings map is results or previous statement, possibly null * use case: optional(A, B) map = relevant subset of results of A * for main query and subquery */ - Mappings eval(Node gNode, Query q, Mapping m, Mappings map) throws SparqlException { + Mappings eval(Node gNode, Query q, Mapping m, Mappings map) { init(q); if (q.isValidate()) { // just compile and complete query return getResults(); } if (q.isCheck()) { - // Draft - Checker check = Checker.create(this); + QueryDefinitionChecker check = QueryDefinitionChecker.create(this); check.check(q); } if (!q.isFail()) { - queryWE(gNode, q, m, map); + query(gNode, q, m, map); if (q.getQueryProfile() == Query.COUNT_PROFILE) { countProfile(); } else { - if (q.isAlgebra()) { - getMemory().setResults(getResults()); - completeSparql.complete(getProducer(), getResults()); - } aggregate(); // order by complete(); - template(); } } @@ -294,28 +267,14 @@ Mappings eval(Node gNode, Query q, Mapping m, Mappings map) throws SparqlExcepti return getResults(); } - void queryWE(Node gNode, Query q, Mapping m, Mappings map) throws SparqlException { - try { - query(gNode, q, m, map); - } catch (SparqlException ex) { - if (ex.isStop()) { - // LDScriptException stopped means stopped query processing - return; - } - // exception means this is an error - throw ex; - } - } - /** * Mapping m is binding parameter, possibly null - * a) from template call with parameter: st:call-template(st:name, ?x, ?y) - * b) from query exec call with parameter: exec.query(q, m) + * Supplied bindings are applied before the main query or subquery is evaluated. * Mappings map is results or previous statement, possibly null * use case: optional(A, B) map = relevant subset of results of A */ @SuppressWarnings("java:S1845") // Query execution method named query by convention - void query(Node gNode, Query q, Mapping m, Mappings map) throws SparqlException { + void query(Node gNode, Query q, Mapping m, Mappings map) { if (m != null) { // bind mapping variables into memory bind(m); @@ -334,11 +293,10 @@ void query(Node gNode, Query q, Mapping m, Mappings map) throws SparqlException /** * Bind external values one by one in memory and eval one by one */ - void queryWithValues(Node gNode, Query q, Mappings map) - throws SparqlException { + void queryWithValues(Node gNode, Query q, Mappings map) { Exp values = q.getValues(); - if (!values.isPostpone() && !q.isAlgebra()) { + if (!values.isPostpone()) { for (Mapping m : values.getMappings()) { if (stopped) { return; @@ -353,11 +311,10 @@ void queryWithValues(Node gNode, Query q, Mappings map) eval(gNode, q, map); } - void eval(Node gNode, Query q, Mappings map) throws SparqlException { + void eval(Node gNode, Query q, Mappings map) { evalExp(gNode, q.getBody(), map); } - void evalExp(Node gNode, Exp exp, Mappings map) - throws SparqlException { + void evalExp(Node gNode, Exp exp, Mappings map) { Stack stack = Stack.create(exp); set(stack); eval(getProducer(), gNode, stack, map, 0); @@ -372,11 +329,6 @@ void countProfile() { getResults().add(m); } - // draft for processing EXTERN expression - public void add(Plugin p) { - plugin = p; - } - public void setMappings(Mappings lMap) { initialResults = lMap; } @@ -387,7 +339,7 @@ public void setMappings(Mappings lMap) { * for optional, minus, union, join, graph * not for subquery which has its own processing */ - Mappings subEval(Query q, Node gNode, Stack stack, Mappings map, int n) throws SparqlException { + Mappings subEval(Query q, Node gNode, Stack stack, Mappings map, int n) { setSubEval(true); starter(q); eval(getProducer(), gNode, stack, map, n); @@ -404,41 +356,24 @@ Mappings subEval(Query q, Node gNode, Stack stack, Mappings map, int n) throws S */ // use case: optional, join, minus - public Mappings subEval(Producer p, Node gNode, Node queryNode, Exp exp, Exp main, Mappings map) throws SparqlException { + public Mappings subEval(Producer p, Node gNode, Node queryNode, Exp exp, Exp main, Mappings map) { return subEval(p, gNode, queryNode, exp, main, map, null, false, false); } - /** - * Evaluates an EXISTS graph pattern against the bindings of the current solution. - * The nested evaluation receives a fresh memory while copying every currently - * bound query node, so correlated variables retain SPARQL EXISTS semantics. - public boolean exists(Producer producer, Node graphNode, Exp pattern) throws SparqlException { - return exists(producer, graphNode, getMemory(), pattern); - } - /** * Evaluates an EXISTS graph pattern against the bindings of the current solution. * The nested evaluation receives a fresh memory while copying every currently * bound query node from the active environment, so correlated variables retain * SPARQL EXISTS semantics even when evaluated inside an OPTIONAL or aggregate context. */ - public boolean exists(Producer producer, Node graphNode, Environment env, Exp pattern) throws SparqlException { + public boolean exists(Producer producer, Node graphNode, Environment env, Exp pattern) { Memory nestedMemory = new Memory(match, evaluator); evaluator.init(nestedMemory); nestedMemory.init(getQuery()); switch (env) { - case Mapping map -> { - nestedMemory.setAppxSearchEnv(getMemory().getAppxSearchEnv()); - nestedMemory.copy(map, pattern); - } - case Memory mem -> { - nestedMemory.setAppxSearchEnv(mem.getAppxSearchEnv()); - mem.copyInto(nestedMemory, pattern); - } - case null, default -> { - nestedMemory.setAppxSearchEnv(getMemory().getAppxSearchEnv()); - getMemory().copyInto(nestedMemory, pattern); - } + case Mapping map -> nestedMemory.copy(map, pattern); + case Memory mem -> mem.copyInto(nestedMemory, pattern); + case null, default -> getMemory().copyInto(nestedMemory, pattern); } Eval nested = copy(nestedMemory, producer); @@ -460,13 +395,12 @@ public boolean exists(Producer producer, Node graphNode, Environment env, Exp pa */ @SuppressWarnings("java:S107") // Internal KGRAM sub-evaluation method requires 9 execution parameters Mappings subEval(Producer p, Node graphNode, Node queryGraphNode, Exp exp, Exp main, - Mappings map, Mapping m, boolean bind, boolean external) throws SparqlException { + Mappings map, Mapping m, boolean bind, boolean external) { Memory mem = new Memory(match, getEvaluator()); getEvaluator().init(mem); // share Binding (global variable, context, etc.) mem.share(getMemory()); mem.init(getQuery()); - mem.setAppxSearchEnv(getMemory().getAppxSearchEnv()); Eval eval = copy(mem, p); if (external) { if (queryGraphNode != null) { @@ -494,7 +428,7 @@ void bind(Memory freshMemory, Exp exp, Exp main, Mapping m, boolean bind) { freshMemory.push(m, -1); } - // QuerySorter may have computed exp node list candidate for binding + // QueryPlanner may have computed exp node list candidate for binding if (main.isGraph() && main.getNodeList() != null) { bindExpNodeList(freshMemory, main, main.getGraphName()); } else if ((bind || main.isBinary()) && exp.getNodeList() != null) { @@ -510,7 +444,7 @@ void bind(Memory freshMemory, Exp exp, Exp main, Mapping m, boolean bind) { * Bind relevant nodes of exp from memory to fresh memory * except may be a graphNode: do not bind it here * because it is bound somewhere else by subEval - * getNodeList() is computer by QuerySorter + * getNodeList() is computer by QueryPlanner */ void bindExpNodeList(Memory freshMemory, Exp exp, Node except) { for (Node qnode : exp.getNodeList()) { @@ -567,26 +501,21 @@ void bindService(Exp exp, Memory freshMemory) { * exp stack */ public Eval copy(Memory m, Producer p) { - return copy(m, p, getEvaluator(), query, false); + return copy(m, p, getEvaluator(), false); } // extern = true if the statement to evaluate is LDScript query: // let (select where) public Eval copy(Memory m, Producer p, boolean extern) { - return copy(m, p, getEvaluator(), query, extern); + return copy(m, p, getEvaluator(), extern); } - // q may be the subQuery - Eval copy(Memory m, Producer p, Evaluator e, Query q, boolean extern) { + Eval copy(Memory m, Producer p, Evaluator e, boolean extern) { Eval ev = create(p, e, getMatcher()); ev.pushEdgeMappings = pushEdgeMappings; ev.parameterGraphMappings = parameterGraphMappings; ev.parameterUnionMappings = parameterUnionMappings; ev.joinMappings = joinMappings; - if (q != null) { - ev.complete(q); - } - ev.setSPARQLEngine(getSPARQLEngine()); ev.setMemory(m); ev.set(getProvider()); if (!extern || getVisitor().isShareable()) { @@ -697,17 +626,9 @@ void initMemory(Query q) { } getProducer().init(q); evaluator.start(memory); - if (q.isAlgebra()) { - complete(q); - } } } - void complete(Query q) { - evalSparql = new EvalSPARQL(q, this); - completeSparql = new CompleteSPARQL(q, this); - } - void profile(Query q) { // select count(*) as ?c where pattern // do not built Mapping, just count them @@ -764,10 +685,6 @@ private void aggregate() { results.aggregate(evaluator, memory, getProducer()); } - private void template() { - results.template(evaluator, memory, getProducer()); - } - /** * We can bind nodes before processing query */ @@ -790,44 +707,6 @@ void bind(Mapping map) { } } - private PathFinder getPathFinder(Exp exp, Producer p) { - List lp = lPathFinder; - for (PathFinder pf : lp) { - if (pf.getEdge() == exp.getEdge()) { - return pf; - } - } - PathFinder pathFinder = PathFinder.create(this, p, query); - - pathFinder.set(getListener()); - pathFinder.setList(query.getGlobalQuery().isListPath()); - // rdf:type/rdfs:subClassOf* generated system path does not store the list of edges - // to be optimized - pathFinder.setStorePath(query.getGlobalQuery().isStorePath() && !exp.isSystem()); - pathFinder.setCache(query.getGlobalQuery().isCachePath()); - // Note: subQuery - pathFinder.setCheckLoop(query.isCheckLoop()); - pathFinder.setCountPath(query.isCountPath()); - pathFinder.init(exp.getRegex(), exp.getMin(), exp.getMax()); - // Note: check this with clean() - if (p.getMode() != Producer.EXTENSION && p.getQuery() == memory.getQuery()) { - // do nothing - } else { - lPathFinder.add(pathFinder); - } - return pathFinder; - } - - /** - * What should be done before throw LimitException - close path threads if - * any - */ - public void clean() { - for (PathFinder pf : lPathFinder) { - pf.stop(); - } - } - private int solution(Producer p, Mapping m, int n) { int backtrack = n - 1; int status = store(p, m); @@ -835,13 +714,11 @@ private int solution(Producer p, Mapping m, int n) { return STOP; } if (results.size() >= limit) { - clean(); // backjump to send finish events to listener // and perform 'close instructions' if any return STOP; } if (!getVisitor().limit(results)) { - clean(); return STOP; } if (backjump != -1) { @@ -863,7 +740,7 @@ private int solution(Producer p, Mapping m, int n) { /** * gNode is a graph name URI if any, may be null */ - int eval(Producer p, Node gNode, Stack stack, int n) throws SparqlException { + int eval(Producer p, Node gNode, Stack stack, int n) { return eval(p, gNode, stack, null, n); } @@ -881,7 +758,7 @@ int eval(Producer p, Node gNode, Stack stack, int n) throws SparqlException { * Eventually, and() edge() path() transform Mappings map into values clause */ @SuppressWarnings("java:S3776") // Core KGRAM evaluation dispatch loop coordinating stack traversal and backtracking - int eval(Producer p, Node graphNode, Stack stack, Mappings map, int n) throws SparqlException { + int eval(Producer p, Node graphNode, Stack stack, Mappings map, int n) { int backtrack = n - 1; boolean isEvent = hasEvent; Memory env = getMemory(); @@ -910,127 +787,99 @@ int eval(Producer p, Node graphNode, Stack stack, Mappings map, int n) throws Sp // A false filter was detected at compile time, or this expression was // identified as always failing: no use to evaluate it. if (!exp.isFail()) { - if (exp.isBGPAble()) { - // @deprecated - // evaluate and record result for next time - // template optimization - exp.setBGPAble(false); - backtrack = bgpAble(p, graphNode, exp, stack, n); - exp.setBGPAble(true); - } else { - // draft test not used - if (query.getGlobalQuery().isAlgebra()) { - Type type = exp.type(); - if (type == Type.BGP || type == Type.JOIN || type == Type.MINUS || type == Type.OPTIONAL || type == Type.GRAPH || type == Type.UNION) { - process(graphNode, p, exp); - return backtrack; - } - } - - if (hasStatement) { - getVisitor().statement(this, getGraphNode(graphNode), exp); - } - - if (!isJoinMappings()) { - // for testing and debug - map = null; - } - - switch (exp.type()) { - - case EMPTY: - - eval(p, graphNode, stack, n + 1); - break; - - case AND: - backtrack = and(p, graphNode, exp, stack, map, n); - break; - - case BGP: - backtrack = p.evaluatesBGPWithEdges() - ? and(p, graphNode, exp, stack, map, n) - : bgp(p, graphNode, exp, stack, n); - break; - - case SERVICE: - // @note: map processing is not optimal for service with union - // we pass mappings only for variables that are in-scope in - // both branches of the union - // it can be bypassed with values var (undef) - backtrack = service(p, graphNode, exp, map, stack, n); - break; - - case GRAPH: - backtrack = - evalGraphNew.eval(p, graphNode, exp, map, stack, n); - break; - - case UNION: - backtrack = union(p, graphNode, exp, map, stack, n); - break; - - case OPTIONAL: - backtrack = optional.eval(p, graphNode, exp, map, stack, n); - break; - case MINUS: - backtrack = minus(p, graphNode, exp, map, stack, n); - break; - case JOIN: - backtrack = join.eval(p, graphNode, exp, map, stack, n); - break; - case QUERY: - backtrack = query(p, graphNode, exp, map, stack, n); - break; - case FILTER: - backtrack = filter(p, graphNode, exp, stack, n); - break; - case BIND: - backtrack = bind(p, graphNode, exp, map, stack, n); - break; - - case PATH: - backtrack = path(p, graphNode, exp, map, stack, n); - break; - - case EDGE: - backtrack = edge(p, graphNode, exp, map, stack, n); - break; - - case VALUES: - backtrack = values(p, graphNode, exp, stack, n); - break; - - /* - * ******************************** - * - * Draft extensions - * - */ - case OPT_BIND: - // use case: ?x p ?y FILTER ?t = ?y BIND(?t, ?y) ?z q ?t - // not effective - backtrack = optBind(p, graphNode, exp, stack, n); - break; - - case ACCEPT: - // use case: select distinct ?x where - // check that ?x is distinct - if (optim) { - if (getResults().accept(env.getNode(exp.getNode()))) { - // backjump here when a mapping will be found with this node - // see store() - backjump = n - 1; - backtrack = eval(p, graphNode, stack, n + 1); - } - } else { + if (hasStatement) { + getVisitor().statement(this, getGraphNode(graphNode), exp); + } + + if (!isJoinMappings()) { + // for testing and debug + map = null; + } + + switch (exp.type()) { + + case EMPTY: + + eval(p, graphNode, stack, n + 1); + break; + + case AND: + backtrack = and(p, graphNode, exp, stack, map, n); + break; + + case BGP: + backtrack = p.evaluatesBGPWithEdges() + ? and(p, graphNode, exp, stack, map, n) + : bgp(p, graphNode, exp, stack, n); + break; + + case SERVICE: + // @note: map processing is not optimal for service with union + // we pass mappings only for variables that are in-scope in + // both branches of the union + // it can be bypassed with values var (undef) + backtrack = service(p, graphNode, exp, map, stack, n); + break; + + case GRAPH: + backtrack = + graphEvaluator.eval(p, graphNode, exp, map, stack, n); + break; + + case UNION: + backtrack = union(p, graphNode, exp, map, stack, n); + break; + + case OPTIONAL: + backtrack = optional.eval(p, graphNode, exp, map, stack, n); + break; + case MINUS: + backtrack = minus(p, graphNode, exp, map, stack, n); + break; + case JOIN: + backtrack = join.eval(p, graphNode, exp, map, stack, n); + break; + case QUERY: + backtrack = query(p, graphNode, exp, map, stack, n); + break; + case FILTER: + backtrack = filter(p, graphNode, exp, stack, n); + break; + case BIND: + backtrack = bind(p, graphNode, exp, map, stack, n); + break; + + + case EDGE: + backtrack = edge(p, graphNode, exp, map, stack, n); + break; + + case VALUES: + backtrack = values(p, graphNode, exp, stack, n); + break; + + case OPT_BIND: + // Binding constraints introduced by the filter optimizer. + backtrack = optBind(p, graphNode, exp, stack, n); + break; + + case ACCEPT: + // use case: select distinct ?x where + // check that ?x is distinct + if (optim) { + if (getResults().accept(env.getNode(exp.getNode()))) { + // backjump here when a mapping will be found with this node + // see store() + backjump = n - 1; backtrack = eval(p, graphNode, stack, n + 1); } - break; + } else { + backtrack = eval(p, graphNode, stack, n + 1); + } + break; - default: - break; - } + default: + break; } } @@ -1041,7 +890,7 @@ int eval(Producer p, Node graphNode, Stack stack, Mappings map, int n) throws Sp return backtrack; } - private int minus(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { + private int minus(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) { int backtrack = n - 1; Memory env = getMemory(); @@ -1053,7 +902,7 @@ private int minus(Producer p, Node graphNode, Exp exp, Mappings data, Stack stac return backtrack; } - MappingSet set1 = new MappingSet(env.getQuery(), map1); + MappingSet set1 = new MappingSet(map1); Exp rest = exp.rest(); Mappings minusMappings = set1.prepareMappingsRest(rest); @@ -1061,7 +910,7 @@ private int minus(Producer p, Node graphNode, Exp exp, Mappings data, Stack stac getVisitor().minus(this, getGraphNode(graphNode), exp, map1, map2); - MappingSet set = new MappingSet(env.getQuery(), exp, set1, new MappingSet(memory.getQuery(), map2)); + MappingSet set = new MappingSet(exp, set1, new MappingSet(map2)); set.start(); for (Mapping map : map1) { @@ -1087,7 +936,7 @@ boolean isFederate(Exp exp) { return exp.isRecFederate(); } - private int union(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { + private int union(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) { int backtrack = n - 1; // join(A, union(B, C)) ; map = eval(A).distinct(inscopenodes()) @@ -1106,7 +955,7 @@ private int union(Producer p, Node graphNode, Exp exp, Mappings data, Stack stac /** Continues the enclosing query once for every solution of a UNION branch. */ private int evalUnionMappings( - Producer p, Node graphNode, Stack stack, int n, Mappings mappings, int backtrack) throws SparqlException { + Producer p, Node graphNode, Stack stack, int n, Mappings mappings, int backtrack) { Memory environment = getMemory(); for (Mapping mapping : mappings) { if (stopped) { @@ -1131,7 +980,7 @@ private int evalUnionMappings( * federated case, map is included in copy of exp as a values(var, map) * clause */ - Mappings unionBranch(Producer p, Node graphNode, Exp exp, Exp main, Mappings data) throws SparqlException { + Mappings unionBranch(Producer p, Node graphNode, Exp exp, Exp main, Mappings data) { if (exp.isFirstWith()) { // union in union: eval inner union with parameter data as is return subEval(p, graphNode, null, exp, main, data); @@ -1152,7 +1001,7 @@ Mappings unionBranch(Producer p, Node graphNode, Exp exp, Exp main, Mappings dat Mappings unionData(Exp exp, Mappings data) { if (data != null) { if (data.getNodeList() == null) { - MappingSet ms = new MappingSet(getQuery(), data); + MappingSet ms = new MappingSet(data); return ms.prepareMappingsRest(exp); } else if (data.getJoinMappings() != null) { // select relevant variables from original join Mappings @@ -1163,7 +1012,7 @@ Mappings unionData(Exp exp, Mappings data) { } - private int and(Producer p, Node graphNode, Exp exp, Stack stack, Mappings data, int n) throws SparqlException { + private int and(Producer p, Node graphNode, Exp exp, Stack stack, Mappings data, int n) { getVisitor().bgp(this, getGraphNode(graphNode), exp, null); if (data != null && exp.size() > 0 && exp.get(0).isEdgePath()) { @@ -1179,7 +1028,7 @@ private int and(Producer p, Node graphNode, Exp exp, Stack stack, Mappings data, } } - private int bgp(Producer p, Node graphNode, Exp exp, Stack stack, int n) throws SparqlException { + private int bgp(Producer p, Node graphNode, Exp exp, Stack stack, int n) { int backtrack = n - 1; List from = getQuery().getFrom(graphNode); Mappings map = p.getMappings(graphNode, from, exp, getMemory()); @@ -1201,7 +1050,7 @@ private int bgp(Producer p, Node graphNode, Exp exp, Stack stack, int n) throws return backtrack; } - private int service(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { + private int service(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) { int backtrack = n - 1; Memory env = getMemory(); Node serv = exp.first().getNode(); @@ -1244,7 +1093,7 @@ void complete(Query q, Mapping map, boolean addNode) { int i = 0; for (Node node : map.getQueryNodes()) { Node out = q.getOuterNode(node); - // draft: use case ?_server_0 + // Preserve auxiliary service bindings absent from the outer projection. if (out == null) { out = node; if (addNode && !q.getSelect().contains(node)) { @@ -1270,7 +1119,7 @@ Node getGraphNode(Node node) { /** * bind(exp as var) */ - private int bind(Producer p, Node graphNode, Exp exp, Mappings map, Stack stack, int n) throws SparqlException { + private int bind(Producer p, Node graphNode, Exp exp, Mappings map, Stack stack, int n) { if (exp.isFunctional()) { return extBind(p, graphNode, exp, stack, n); } @@ -1306,7 +1155,7 @@ private int bind(Producer p, Node graphNode, Exp exp, Mappings map, Stack stack, * values (?x ?y) { unnest(exp) } * compiled as extended bind */ - private int extBind(Producer p, Node graphNode, Exp exp, Stack stack, int n) throws SparqlException { + private int extBind(Producer p, Node graphNode, Exp exp, Stack stack, int n) { int backtrack = n - 1; Memory env = getMemory(); env.setGraphNode(graphNode); @@ -1344,7 +1193,7 @@ HashMap toMap(List list) { /** * */ - private int filter(Producer p, Node graphNode, Exp exp, Stack stack, int n) throws SparqlException { + private int filter(Producer p, Node graphNode, Exp exp, Stack stack, int n) { int backtrack = n - 1; Memory env = getMemory(); boolean success = true; @@ -1439,99 +1288,7 @@ public Mappings eval(Filter f, Environment env, Producer p, List nodes) { return getProducer().map(nodes, res, n); } - /** - * Enumerate candidate edges - * exp = query edge - * graphNode = null for default graph triple pattern - * graphNode = URI of named graph for named graph triple pattern - * graph ?g { exp } evaluates exp for each value of ?g = g_i - * in this case graphNode = g_i and from is useless here - * from named has been used in named graph pattern evaluator - * data is possible relevant bindings coming from preceding statement evaluation - */ - @SuppressWarnings("java:S3776") // Core KGRAM property path evaluation loop managing automaton candidate matching and backtracking - private int path(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { - int backtrack = n - 1; - int evENUM = Event.ENUM; - PathFinder path = getPathFinder(exp, p); - Memory env = getMemory(); - Query qq = getQuery(); - boolean isEvent = hasEvent; - - if (data != null && data.getNodeList() != null && isPushEdgeMappings()) { - // push values(data) before edge in stack - if (logger.isInfoEnabled()) { - logger.info("Push path mappings:\nvalue {}\n{}", - data.getNodeList(), data.toString(false, false, DISPLAY_RESULT_LIMIT)); - } - return eval(p, graphNode, stack.addCopy(n, exp.getValues(data)), n); - } - - Filter f = getFilterForPath(stack, n); - path.start(exp.getEdge(), qq.getPathNode(), env, f); - boolean isSuccess = false; - - List list = getGraphNodeList(p, graphNode, env, qq); - Node backtrackNode = getBacktrackNode(p, graphNode, env); - - for (Mapping map : path.candidate(graphNode, list, env)) { - if (stopped) { - path.stop(); - return STOP; - } - boolean success = match(map) && env.push(map, n); - - if (isEvent) { - send(evENUM, exp, map, success); - } - - if (success) { - isSuccess = true; - backtrack = eval(p, graphNode, stack, n + 1); - env.pop(map); - - if (backtrack < n) { - path.stop(); - // remove it to get fresh automaton next time - lPathFinder.remove(path); - return backtrack; - } - } - } - - if (!isSuccess && optim) { - // backjump to max index where nodes are bound for first time: - backtrack = env.getIndex(backtrackNode, exp.getEdge()); - } - path.stop(); - return backtrack; - } - - private Filter getFilterForPath(Stack stack, int n) { - if (stack.size() > n + 1 && stack.get(n + 1).isFilter()) { - return stack.get(n + 1).getFilter(); - } - return null; - } - - private Node getBacktrackNode(Producer p, Node graphNode, Memory env) { - if (p.getMode() == Producer.EXTENSION) { - if (p.getQuery() == env.getQuery()) { - return p.getGraphNode(); - } - return null; - } - return graphNode; - } - - private List getGraphNodeList(Producer p, Node graphNode, Memory env, Query qq) { - if (p.getMode() == Producer.EXTENSION && p.getQuery() == env.getQuery()) { - return empty; - } - return qq.getFrom(graphNode); - } - - private int values(Producer p, Node graphNode, Exp exp, Stack stack, int n) throws SparqlException { + private int values(Producer p, Node graphNode, Exp exp, Stack stack, int n) { int backtrack = n - 1; getVisitor().values(this, getGraphNode(graphNode), exp, exp.getMappings()); @@ -1600,7 +1357,7 @@ void free(List varList, Mapping map) { * data may be null */ @SuppressWarnings("java:S3776") // Core KGRAM edge evaluation algorithm managing graph matching and backjump optimization - private int edge(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { + private int edge(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) { int backtrack = n - 1; int evENUM = Event.ENUM; boolean isSuccess = false; @@ -1615,7 +1372,7 @@ private int edge(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack // for default graph triple pattern, consider select from if any // for named graph triple pattern, from named is useless here because graphNode = URI of one named graph List graphNodeList = qq.getFrom(graphNode); - // the backtrack graphNode (deprecated) + // Graph binding used to determine the backtracking level. Node backtrackGraphNode = graphNode; boolean matchNBNode = qEdge.isMatchArity(); @@ -1639,7 +1396,7 @@ private int edge(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack Iterable entities; if (hasProduce) { - // draft not used + // A query visitor may provide candidates before the producer is consulted. entities = produce(graphNode, qEdge); if (entities == null) { entities = p.getEdges(graphNode, graphNodeList, qEdge, env); @@ -1717,7 +1474,7 @@ private int edge(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack * select * where {{select * where {?x p ?y}} . ?y q ?z} * new eval, new memory, share only sub query select variables */ - private int query(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { + private int query(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) { int backtrack = n - 1; int evENUM = Event.ENUM; boolean isEvent = hasEvent; @@ -1727,7 +1484,7 @@ private int query(Producer p, Node graphNode, Exp exp, Mappings data, Stack stac // copy current Eval, new stack // bind sub query select nodes in new memory - Eval ev = copy(copyMemory(env, getQuery(), subQuery, null), p, getEvaluator(), subQuery, false); + Eval ev = copy(copyMemory(env, getQuery(), subQuery, null), p, getEvaluator(), false); Mappings lMap = ev.eval(graphNode, subQuery, null, selectQueryMappings(data)); getVisitor().query(this, getGraphNode(graphNode), exp, lMap); @@ -1869,7 +1626,7 @@ void store(Mapping ans) { } boolean acceptable(Mapping m) { - return getQuery().getGlobalQuery().isAlgebra() || getResults().acceptable(m); + return getResults().acceptable(m); } @@ -1893,22 +1650,6 @@ private boolean push(Producer p, Edge qEdge, Edge ent, int n) { } - - // for path - private boolean match(Mapping map) { - int i = 0; - Memory env = getMemory(); - Matcher mm = getMatcher(); - for (Node qNode : map.getQueryNodes()) { - Node node = map.getNode(i++); - if (!mm.match(qNode, node, env)) { - return false; - } - } - return true; - } - - public void createManager() { if (manager == null) { setEventManager(new KgramEventDispatcher()); @@ -1953,14 +1694,6 @@ public Stack getStack() { return current; } - public SPARQLEngine getSPARQLEngine() { - return sparqlEngine; - } - - public void setSPARQLEngine(SPARQLEngine sparqlEngine) { - this.sparqlEngine = sparqlEngine; - } - public ProcessVisitor getVisitor() { return visitor; } @@ -1980,37 +1713,18 @@ public void setStop(boolean stopped) { - /* - * ************************************************************************* - * - * Alternative interpreter - * + /** + * Requests cooperative termination, including active join, graph, and optional + * evaluation. May be called by the timeout watchdog; remaining evaluation state + * must only be accessed by the owning execution thread. */ - public void finish() { setStop(true); join.setStop(); - evalGraphNew.setStop(); + graphEvaluator.setStop(); optional.setStop(true); } - /** - * SPARQL algebra requires kgram to compute BGP exp and return Mappings - * List from = query.getFrom(gNode); Mappings map = - * p.getMappings(gNode, from, exp, memory); - */ - Mappings exec(Node gNode, Producer p, Exp exp) throws SparqlException { - List from = query.getFrom(gNode); - return p.getMappings(gNode, from, exp, memory); - } - - /** - * Evaluate exp with SPARQL Algebra on Mappings, not with Memory stack - */ - void process(Node gNode, Producer p, Exp exp) { - results = evalSparql.eval(gNode, p, exp); - } - /** * use case: *

@@ -2019,7 +1733,7 @@ void process(Node gNode, Producer p, Exp exp) { *

* (n) BIND {?x := ?y} (n+1) EDGE{?x ?q ?z} (n+2) FILTER{?x = ?y} */ - private int optBind(Producer p, Node gNode, Exp exp, Stack stack, int n) throws SparqlException { + private int optBind(Producer p, Node gNode, Exp exp, Stack stack, int n) { Memory env = getMemory(); int backtrack; if (exp.isBindCst()) { @@ -2062,7 +1776,7 @@ private int optBind(Producer p, Node gNode, Exp exp, Stack stack, int n) throws /** * exp : BIND{?x = cst1 || ?x = cst2} Bind ?x with all its values */ - private int cbind(Producer p, Node gNode, Exp exp, Stack stack, int n) throws SparqlException { + private int cbind(Producer p, Node gNode, Exp exp, Stack stack, int n) { Node qNode = exp.get(0).getNode(); if (!exp.status() || memory.isBound(qNode)) { return eval(p, gNode, stack, n + 1); @@ -2092,7 +1806,7 @@ private void initBindNodes(Exp exp, Producer prod) { } } - private int evalBindNodes(Producer p, Node gNode, Exp exp, Stack stack, int n, Node qNode) throws SparqlException { + private int evalBindNodes(Producer p, Node gNode, Exp exp, Stack stack, int n, Node qNode) { Memory env = memory; for (Node node : exp.getNodeList()) { env.push(qNode, node, n); @@ -2108,60 +1822,6 @@ private int evalBindNodes(Producer p, Node gNode, Exp exp, Stack stack, int n, N return n - 1; } - /** - * Exp evaluated as a BGP, get result Mappings, push Mappings and continue - * Use case: cache the Mappings - * - * @deprecated - */ - @Deprecated(since = "4.0.0") - @SuppressWarnings("java:S1133") - private int bgpAble(Producer p, Node graphNode, Exp exp, Stack stack, int n) throws SparqlException { - int backtrack = n - 1; - Mappings map = getMappings(p, graphNode, exp); - for (Mapping m : map) { - if (stopped) { - return STOP; - } - m.fixQueryNodes(getQuery()); - boolean b = getMemory().push(m, n, false); - if (b) { - int back = eval(p, graphNode, stack, n + 1); - getMemory().pop(m); - if (back < n) { - return back; - } - } - } - return backtrack; - } - - /** - * Mappings of exp may be cached for specific query node Use case: datashape - * template exp = graph ?shape {?sh sh:path ?p} exp is evaluated once for - * each value of ?sh and Mappings are cached successive evaluations of exp - * on ?sh get Mappings from cache - * - * @deprecated - */ - @Deprecated(since = "4.0.0") - @SuppressWarnings("java:S1133") - Mappings getMappings(Producer p, Node graphNode, Exp exp) throws SparqlException { - if (exp.hasCache()) { - // @deprecated - Node n = getMemory().getNode(exp.getCacheNode()); - if (n != null) { - Mappings m = exp.getMappings(n); - if (m == null) { - m = subEval(p, graphNode, graphNode, exp, exp, null, null, true, false); - exp.cache(n, m); - } - return m; - } - } - return subEval(p, graphNode, null, exp, exp, null, null, true, false); - } - /** * Draf extension where a Visitor provides Edge iterator */ @@ -2179,18 +1839,6 @@ Iterable produce(Node gNode, Edge edge) { return null; } - /** - * @deprecated Legacy debugging hook - */ - @Override - @Deprecated(since = "4.0.0") - @SuppressWarnings("java:S1133") - public void exec(Exp exp, Environment env, int n) { - if (exp.getObject() instanceof String label && env.getNode(label) != null && logger.isDebugEnabled()) { - logger.debug("{}: {} {}", n, label, env.getNode(label).getLabel()); - } - } - public boolean isJoinMappings() { return joinMappings; } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalGraph.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalGraph.java index ac7aff46e..b04391388 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalGraph.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalGraph.java @@ -32,7 +32,7 @@ void setStop() { * gNode is possible named graphURI stemming from evaluation context * It is not the named graph at stake here */ - int eval(Producer p, Node gNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { + int eval(Producer p, Node gNode, Exp exp, Mappings data, Stack stack, int n) { int backtrack = n - 1; Node graphNode = exp.getGraphName(); Node graph = engine.getNode(p, graphNode); @@ -103,7 +103,7 @@ private boolean isNamedGraph(Producer producer, Node graphNode, Node graph) { * named graph list may come from Mappings map from previous statement * OR from the "from named" clause OR from dataset named graph list */ - private Mappings graphNodes(Producer p, Exp exp, Mappings map) throws SparqlException { + private Mappings graphNodes(Producer p, Exp exp, Mappings map) { Memory env = engine.getMemory(); Query qq = engine.getQuery(); Matcher mm = engine.getMatcher(); @@ -141,7 +141,7 @@ private Mappings graphNodes(Producer p, Exp exp, Mappings map) throws SparqlExce * Node graph: graph URI or Node graph pointer or Node path pointer * Exp exp: graph name { BGP } */ - private Mappings graph(Producer p, Node graph, Exp exp, Mappings map) throws SparqlException { + private Mappings graph(Producer p, Node graph, Exp exp, Mappings map) { boolean external = false; Producer np = p; if (graph != null && p.isProducer(graph)) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalJoin.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalJoin.java index 423291b03..6b7ec8987 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalJoin.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalJoin.java @@ -13,13 +13,18 @@ import static fr.inria.corese.core.next.query.impl.engine.eval.Eval.STOP; /** + * Joins intermediate solutions on the evaluation thread. Cancellation may be + * requested concurrently and is observed through the volatile stop flag. + * * @author corby */ -@SuppressWarnings("java:S3776") // Legacy KGRAM join execution algorithms -public class EvalJoin { +final class EvalJoin { - Eval engine; - volatile boolean stop = false; + // Stack backtracking levels cannot reach this value; no continuation ran. + private static final int NO_CANDIDATE = Integer.MAX_VALUE; + + private final Eval engine; + private volatile boolean stop; EvalJoin(Eval engine) { this.engine = engine; @@ -37,7 +42,7 @@ Query getQuery() { * JOIN(e1, e2) Eval e1, engine e2, generate all joins that are compatible in * cartesian product */ - int eval(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { + int eval(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) { int backtrack = n - 1; Memory env = engine.getMemory(); Mappings map1 = engine.subEval(p, graphNode, graphNode, exp.first(), exp, data); @@ -59,7 +64,7 @@ int eval(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) return STOP; } - MappingSet set1 = new MappingSet(getQuery(), map1Extended); + MappingSet set1 = new MappingSet(map1Extended); Mappings joinMappings = null; if (engine.isJoinMappings()) { joinMappings = set1.prepareMappingsRest(exp.rest()); @@ -76,7 +81,7 @@ int eval(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) } - int join(Producer p, Node graphNode, Stack stack, Memory env, Mappings map1, Mappings map2, int n) throws SparqlException { + int join(Producer p, Node graphNode, Stack stack, Memory env, Mappings map1, Mappings map2, int n) { Node commonVariable = map1.getCommonNode(map2); if (commonVariable == null) { return joinWithoutCommonVariable(p, graphNode, stack, env, map1, map2, n); @@ -86,125 +91,142 @@ int join(Producer p, Node graphNode, Stack stack, Memory env, Mappings map1, Map } /** - * Try to find common variable var in both mappings - * if any: sort second map wrt var - * iterate first map, find occurrence of same value of var by dichotomy in second map - * map1 and map2 share commonVariable - * sort map2 on commonVariable - * enumerate map1 - * retrieve the index of value of commonVariable in map2 by dichotomy + * Sorts the smaller join input by a shared variable and enumerates compatible + * pairs. Unbound values remain compatible with every value on the other side. */ - @SuppressWarnings("java:S107") // Internal KGRAM join execution method requires 8 execution parameters - int joinWithCommonVariable(Node commonVariable, Producer p, Node graphNode, Stack stack, Memory env, Mappings map1, Mappings map2, int n) throws SparqlException { - int backtrack = n - 1; - if (map1.size() > map2.size()) { - Mappings tmp = map1; - map1 = map2; - map2 = tmp; + @SuppressWarnings("java:S107") // The shared variable accompanies the existing evaluation frame. + int joinWithCommonVariable(Node commonVariable, Producer producer, Node graphNode, Stack stack, + Memory environment, Mappings left, Mappings right, int level) { + if (left.size() > right.size()) { + Mappings temporary = left; + left = right; + right = temporary; } - // Enable comparison customization through the query-scoped visitor. - map2.setEval(engine); - map2.sort(commonVariable); - - for (Mapping m1 : map1) { + right.setEval(engine); + right.sort(commonVariable); + int backtrack = level - 1; + for (Mapping candidate : left) { if (stop) { return STOP; } + Node value = candidate.getNode(commonVariable); + if (!environment.push(candidate, level)) { + continue; + } + int continuation = value == null + ? joinAllCandidates(producer, graphNode, stack, environment, right, level) + : joinCandidatesForBinding(value, commonVariable, producer, graphNode, stack, right, level); + if (continuation != NO_CANDIDATE) { + backtrack = continuation; + if (backtrack < level) { + return backtrack; + } + } + environment.pop(candidate); + } + return backtrack; + } - Node n1 = m1.getNode(commonVariable); - if (env.push(m1, n)) { - - if (n1 == null) { - // enumerate all map2 - for (Mapping m2 : map2) { - if (stop) { - return STOP; - } - if (env.push(m2, n)) { - backtrack = engine.eval(p, graphNode, stack, n + 1); - env.pop(m2); - if (backtrack < n) { - return backtrack; - } - } - } - } else { - // first, try : n2 == null - for (Mapping m2 : map2) { - if (stop) { - return STOP; - } - Node n2 = m2.getNode(commonVariable); - if (n2 != null) { - break; - } - if (env.push(m2, n)) { - backtrack = engine.eval(p, graphNode, stack, n + 1); - env.pop(m2); - if (backtrack < n) { - return backtrack; - } - } - } - - // second, try : n2 != null - int nn = map2.find(n1, commonVariable); - if (nn >= 0 && nn < map2.size()) { - - for (int i = nn; i < map2.size(); i++) { - // get value of var in map2 - Mapping m2 = map2.get(i); - Node n2 = m2.getNode(commonVariable); - - if (n2 == null || !n1.match(n2)) { - // map2 is sorted, if n1 != n2 we can exit the loop - break; - } else if (env.push(m2, n)) { - backtrack = engine.eval(p, graphNode, stack, n + 1); - env.pop(m2); - if (backtrack < n) { - return backtrack; - } - } - } + /** Enumerates the Cartesian product when the inputs have no shared variable. */ + int joinWithoutCommonVariable(Producer producer, Node graphNode, Stack stack, Memory environment, + Mappings left, Mappings right, int level) { + int backtrack = level - 1; + for (Mapping candidate : left) { + if (stop) { + return STOP; + } + if (environment.push(candidate, level)) { + int continuation = joinAllCandidates(producer, graphNode, stack, environment, right, level); + if (continuation != NO_CANDIDATE) { + backtrack = continuation; + if (backtrack < level) { + return backtrack; } } - env.pop(m1); + environment.pop(candidate); } } return backtrack; } - /** - * No variable in common: cartesian product of mappings - */ - int joinWithoutCommonVariable(Producer p, Node graphNode, Stack stack, Memory env, Mappings map1, Mappings map2, int n) throws SparqlException { - int backtrack = n - 1; - for (Mapping m1 : map1) { + private int joinAllCandidates(Producer producer, Node graphNode, Stack stack, Memory environment, + Mappings candidates, int level) { + int backtrack = NO_CANDIDATE; + for (Mapping candidate : candidates) { if (stop) { return STOP; } - if (env.push(m1, n)) { + if (environment.push(candidate, level)) { + backtrack = evaluateCandidate(producer, graphNode, stack, environment, candidate, level); + if (backtrack < level) { + return backtrack; + } + } + } + return backtrack; + } - for (Mapping m2 : map2) { - if (stop) { - return STOP; - } - if (env.push(m2, n)) { - backtrack = engine.eval(p, graphNode, stack, n + 1); - env.pop(m2); - if (backtrack < n) { - return backtrack; - } - } + private int joinCandidatesForBinding(Node value, Node variable, Producer producer, Node graphNode, + Stack stack, Mappings candidates, int level) { + int unbound = joinUnboundCandidates(variable, producer, graphNode, stack, candidates, level); + if (unbound != NO_CANDIDATE && unbound < level) { + return unbound; + } + int bound = joinBoundCandidates(value, variable, producer, graphNode, stack, candidates, level); + return bound == NO_CANDIDATE ? unbound : bound; + } + + private int joinUnboundCandidates(Node variable, Producer producer, Node graphNode, Stack stack, + Mappings candidates, int level) { + Memory environment = engine.getMemory(); + int backtrack = NO_CANDIDATE; + for (Mapping candidate : candidates) { + if (stop) { + return STOP; + } + if (candidate.getNode(variable) != null) { + break; + } + if (environment.push(candidate, level)) { + backtrack = evaluateCandidate(producer, graphNode, stack, environment, candidate, level); + if (backtrack < level) { + return backtrack; } + } + } + return backtrack; + } - env.pop(m1); + private int joinBoundCandidates(Node value, Node variable, Producer producer, Node graphNode, + Stack stack, Mappings candidates, int level) { + Memory environment = engine.getMemory(); + int backtrack = NO_CANDIDATE; + int start = candidates.find(value, variable); + if (start < 0) { + return backtrack; + } + for (int index = start; index < candidates.size(); index++) { + Mapping candidate = candidates.get(index); + Node candidateValue = candidate.getNode(variable); + if (candidateValue == null || !value.match(candidateValue)) { + break; + } + if (environment.push(candidate, level)) { + backtrack = evaluateCandidate(producer, graphNode, stack, environment, candidate, level); + if (backtrack < level) { + return backtrack; + } } } return backtrack; } + private int evaluateCandidate(Producer producer, Node graphNode, Stack stack, Memory environment, + Mapping candidate, int level) { + int backtrack = engine.eval(producer, graphNode, stack, level + 1); + environment.pop(candidate); + return backtrack; + } // use case: join(and(values ?s { }), service ?s { }) boolean isFirstWithValuesOnly(Exp exp) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalOptional.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalOptional.java index 4513b9820..184ced3ab 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalOptional.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalOptional.java @@ -49,7 +49,7 @@ public void setStop(boolean stop) { * merge ... */ @SuppressWarnings("java:S3776") - int eval(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { + int eval(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) { int backtrack = n - 1; Memory env = engine.getMemory(); @@ -63,7 +63,7 @@ int eval(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) Mappings map2; Exp rest = exp.rest(); - MappingSet set1 = new MappingSet(getQuery(), map1); + MappingSet set1 = new MappingSet(map1); Mappings map = set1.prepareMappingsRest(rest); if (map != null && exp.getInscopeFilter() != null) { @@ -96,8 +96,8 @@ int eval(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) engine.getVisitor().optional(engine, engine.getGraphNode(graphNode), exp, map1, map2); - MappingSet set = new MappingSet(getQuery(), exp, set1, - new MappingSet(getQuery(), map2)); + MappingSet set = new MappingSet(exp, set1, + new MappingSet(map2)); set.start(); for (Mapping m1 : map1) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalSPARQL.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalSPARQL.java deleted file mode 100644 index 3bff77776..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalSPARQL.java +++ /dev/null @@ -1,219 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.eval; -import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; -import fr.inria.corese.core.next.query.impl.engine.pattern.Query; -import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; -import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; - -import fr.inria.corese.core.next.query.impl.engine.model.Node; -import fr.inria.corese.core.next.query.impl.engine.model.ExpType.Type; -import fr.inria.corese.core.next.query.impl.engine.spi.Producer; -import fr.inria.corese.core.next.data.api.model.DatatypeValue; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; - -/** - * SPARQL Statements implemented as SPARQL Algebra on Mappings - * Alternative interpreter not used - * - * @author Olivier Corby, Wimmics INRIA I3S, 2016 - */ -public class EvalSPARQL { - - private static final Logger logger = LoggerFactory.getLogger(EvalSPARQL.class); - - Eval engine; - Query query; - - EvalSPARQL(Query q, Eval e) { - engine = e; - query = q; - } - - Mappings eval(Node graph, Producer p, Exp exp) { - return eval(graph, p, exp, null); - } - - Mappings eval(Node graph, Producer p, Exp exp, Mapping m) { - - return switch (exp.type()) { - case BGP -> bgp(graph, p, exp, m); - case JOIN -> join(graph, p, exp, m); - case UNION -> union(graph, p, exp, m); - case MINUS -> minus(graph, p, exp, m); - case OPTIONAL -> optional(graph, p, exp, m); - case GRAPH -> graph(p, exp, m); - default -> Mappings.create(query); - }; - } - - - Mappings join(Node graph, Producer p, Exp exp, Mapping m) { - Mappings m1 = eval(graph, p, exp.first(), m); - if (exp.rest().isBGPFilter()) { - return filter(p, exp.rest(), m1); - } else { - Mappings m2 = eval(graph, p, exp.rest(), m); - return join(m1, m2); - } - } - - Mappings join(Mappings map1, Mappings map2) { - if (map1.isEmpty()) { - return map1; - } - if (map2.isEmpty()) { - return map2; - } - - Mapping m1 = map1.get(0); - Mapping m2 = map2.get(0); - - // common variable - Node cmn = m1.getCommonNode(m2); - - if (cmn == null) { - return map1.joiner(map2); - } - - // sort map2 according to common variable, null value first - map2.sort(engine, cmn); - return map1.joiner(map2, cmn); - } - - Mappings graph(Producer p, Exp exp, Mapping m) { - return bgp(exp.getGraphName(), p, exp.rest(), m); - } - - Mappings optional(Node graph, Producer p, Exp exp, Mapping mm) { - Mappings m1 = bgp(graph, p, exp.first(), mm); - Mappings m2 = bgp(graph, p, exp.rest()); - Mappings res = Mappings.create(query); - HashMap hm = new HashMap<>(); - - for (Mapping ma : m1) { - int nbsuc = 0; - for (Mapping mb : m2) { - Mapping m = ma.merge(mb); - if (acceptOptionalMapping(graph, p, exp, m, hm)) { - res.add(m); - nbsuc++; - } - } - - if (nbsuc == 0) { - res.add(ma); - } - } - return res; - } - - private boolean acceptOptionalMapping(Node graph, Producer producer, Exp expression, - Mapping mapping, HashMap blankNodes) { - if (mapping == null) { - return false; - } - if (!expression.isPostpone()) { - return true; - } - mapping.setQuery(query); - mapping.setMap(blankNodes); - blankNodes.clear(); - return postpone(graph, expression, mapping, producer); - } - - boolean postpone(Node gNode, Exp exp, Mapping m, Producer p) { - for (Exp e : exp.getPostpone()) { - if (!engine.test(gNode, e.getFilter(), m, p)) { - return false; - } - } - return true; - } - - - Mappings minus(Node graph, Producer p, Exp exp, Mapping mm) { - Mappings m1 = bgp(graph, p, exp.first(), mm); - Mappings m2 = bgp(graph, p, exp.rest()); - Mappings res = Mappings.create(m1.getQuery()); - - for (Mapping m : m1) { - boolean ok = true; - - for (Mapping minus : m2) { - if (m.compatible(minus)) { - ok = false; - break; - } - } - - if (ok) { - res.add(m); - } - } - return res; - } - - - Mappings union(Node graph, Producer p, Exp exp, Mapping m) { - Mappings m1 = bgp(graph, p, exp.first(), m); - Mappings m2 = bgp(graph, p, exp.rest(), m); - Mappings res = m1.union(m2); - res.setQuery(m1.getQuery()); - return res; - } - - Mappings bgp(Node graph, Producer p, Exp exp) { - return bgp(graph, p, exp, null); - } - - Mappings bgp(Node graph, Producer p, Exp exp, Mapping m) { - if (exp.size() == 1) { - Exp body = exp.get(0); - if (body.isBGP()) { - return bgp(graph, p, body, m); - } else if (body.isStatement() && !body.isQuery()) { - return eval(graph, p, body, m); - } - } - return basic(graph, p, exp); - } - - Mappings basic(Node graph, Producer p, Exp exp) { - exp.setType(Type.AND); - try { - return engine.exec(graph, p, exp); - } catch (SparqlException ex) { - logger.error("Error executing basic pattern: {}", ex.getMessage(), ex); - return null; - } finally { - exp.setType(Type.BGP); - } - } - - private Mappings filter(Producer p, Exp exp, Mappings map) { - Mappings res = Mappings.create(map.getQuery()); - HashMap bnode = new HashMap<>(); - for (Mapping m : map) { - m.setMap(bnode); - bnode.clear(); - m.setQuery(map.getQuery()); - if (test(p, exp, m)) { - res.add(m); - } - } - return res; - } - - private boolean test(Producer p, Exp exp, Mapping m) { - for (Exp f : exp) { - if (!engine.test(null, f.getFilter(), m, p)) { - return false; - } - } - return true; - } - - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/Checker.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/QueryDefinitionChecker.java similarity index 75% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/Checker.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/QueryDefinitionChecker.java index d25ad0265..878e6a4a9 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/Checker.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/QueryDefinitionChecker.java @@ -14,7 +14,7 @@ /** - * Query Type Checker + * Query Type QueryDefinitionChecker *

* Check occurrence of properties in Producer * Check if class/property are defined in Producer @@ -23,23 +23,23 @@ * * @author Olivier Corby, Edelweiss, INRIA 2011 */ -public class Checker { +final class QueryDefinitionChecker { - private static final Logger LOGGER = LoggerFactory.getLogger(Checker.class); + private static final Logger LOGGER = LoggerFactory.getLogger(QueryDefinitionChecker.class); - Eval eval; - Producer producer; - Matcher matcher; - Query query; + private final Eval eval; + private final Producer producer; + private final Matcher matcher; + private Query query; - Checker(Eval e) { + QueryDefinitionChecker(Eval e) { eval = e; producer = e.getProducer(); matcher = e.getMatcher(); } - public static Checker create(Eval e) { - return new Checker(e); + static QueryDefinitionChecker create(Eval e) { + return new QueryDefinitionChecker(e); } void check(Query q) { @@ -48,7 +48,7 @@ void check(Query q) { } - void check(Node gNode, Exp exp, Environment env) { + private void check(Node gNode, Exp exp, Environment env) { switch (exp.type()) { @@ -74,7 +74,7 @@ void check(Node gNode, Exp exp, Environment env) { * Check occurrence of edge * If edge has an associated query, check class/property definition in ontology */ - void edge(Node gNode, Exp exp, Environment env) { + private void edge(Node gNode, Exp exp, Environment env) { Edge edge = exp.getEdge(); boolean exist = false; boolean match = false; @@ -94,15 +94,9 @@ void edge(Node gNode, Exp exp, Environment env) { Query q = query.get(edge); if (q != null) { Eval ee = Eval.create(producer, eval.getEvaluator(), matcher); - Mappings map; - try { - map = ee.query(q); - define = !map.isEmpty(); - report(edge, exist, match, define); - } catch (SparqlException failure) { - throw new IllegalStateException( - "Failed to evaluate the edge definition query", failure); - } + Mappings map = ee.query(q); + define = !map.isEmpty(); + report(edge, exist, match, define); } else { report(edge, exist, match); @@ -110,13 +104,13 @@ void edge(Node gNode, Exp exp, Environment env) { } - void report(Edge edge, boolean exist, boolean match, boolean define) { + private void report(Edge edge, boolean exist, boolean match, boolean define) { query.addInfo(edge.toString(), " defined:" + define + " exist: " + exist + " match: " + match); LOGGER.info("Edge: {}: {} {} {}", edge, exist, match, define); } - void report(Edge edge, boolean exist, boolean match) { + private void report(Edge edge, boolean exist, boolean match) { query.addInfo(edge.toString(), " exist: " + exist + " match: " + match); LOGGER.info("Edge: {}: {} {}", edge, exist, match); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/ResultsImpl.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/ResultsImpl.java deleted file mode 100644 index fa490c658..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/ResultsImpl.java +++ /dev/null @@ -1,46 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.eval; - -import fr.inria.corese.core.next.query.impl.engine.model.Node; -import fr.inria.corese.core.next.query.impl.engine.spi.Result; -import fr.inria.corese.core.next.query.impl.engine.spi.Results; -import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; -import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; - -import java.util.Iterator; -import java.util.List; - -public class ResultsImpl implements Results { - - Mappings maps; - - ResultsImpl(Mappings ms){ - maps = ms; - } - - public List getSelect() { - return maps.getSelect(); - } - - @Override - public Iterator iterator() { - Iterator it = maps.iterator(); - return new Iterator<>() { - @Override - public boolean hasNext() { - return it.hasNext(); - } - - @Override - public Result next() { - return it.next(); - } - }; - } - - public int size() { - return maps.size(); - } - - - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/SelectCursor.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/SelectCursor.java new file mode 100644 index 000000000..5bd71da8f --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/SelectCursor.java @@ -0,0 +1,724 @@ +package fr.inria.corese.core.next.query.impl.engine.eval; + +import fr.inria.corese.core.next.query.api.exception.QueryTypeErrorException; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.FallbackReason; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.impl.engine.model.Edge; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.model.ExpType.Type; +import fr.inria.corese.core.next.query.impl.engine.path.PropertyPathEdge; +import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; +import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; +import fr.inria.corese.core.next.query.impl.engine.solution.Memory; +import fr.inria.corese.core.next.query.impl.engine.spi.Producer; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Resumable, depth-first execution of a non-blocking SELECT pipeline on its calling + * thread. Frames retain only the current bindings and candidate iterator at each + * depth. Projection, expression evaluation, RDF matching and DISTINCT use the same + * engine services as full evaluation; accepted mappings are never added to results. + * DISTINCT still needs its semantic seen-set. Unsupported algebra uses full evaluation. + * + *

The owner must close this cursor, including after partial consumption. The + * surrounding execution owns cancellation and the producer's stable read snapshot.

+ */ +public final class SelectCursor implements Iterator, AutoCloseable { + private final Eval eval; + private final Eval cancellation; + private final Mapping parameters; + private final Node scopeGraph; + private final boolean nested; + private final boolean applySlice; + private final Query query; + private final Memory memory; + private final Producer producer; + private final List instructions = new ArrayList<>(); + private final Frame[] frames; + private int depth; + private long accepted; + private long emitted; + private Mapping next; + private boolean closed; + + /** + * Returns the first guard selecting full evaluation, without reading any data. + * NONE means that the algebra is eligible; the executor must still capture an + * independent read snapshot. The decision also drives diagnostics so reporting + * cannot drift from the strategy actually selected. + * + * @param query completed execution query + * @return first materialization guard, or NONE for eligible algebra + */ + public static FallbackReason fallbackReason(Query query) { + if (!query.isSelect()) return FallbackReason.QUERY_FORM; + if (query.isOrderBy()) return FallbackReason.ORDER_BY; + return operatorFallbackReason(query); + } + + /** + * Whether this ordered SELECT can supply unsliced solutions to a bounded selector. + * Aggregate and scope guards remain identical to those of the progressive cursor. + * + * @param query completed execution query + * @return whether ORDER BY has a finite, representable LIMIT plus OFFSET and eligible input + */ + public static boolean supportsOrderedInput(Query query) { + return query.isSelect() && query.isOrderBy() && query.getLimit() < Integer.MAX_VALUE + && (long) query.getLimit() + query.getOffset() <= Integer.MAX_VALUE + && operatorFallbackReason(query) == FallbackReason.NONE; + } + + private static FallbackReason operatorFallbackReason(Query query) { + if (query.hasGroupBy() || query.isAggregate() || query.getHaving() != null) return FallbackReason.AGGREGATION; + return fallbackReason(query.getBody(), query.getValues() != null && !singleton(query.getValues())); + } + + private static FallbackReason fallbackReason(Exp exp, boolean repeated) { + return switch (exp.type()) { + case AND, BGP -> sequenceReason(exp, repeated); + case UNION -> unionReason(exp, repeated); + case JOIN -> joinReason(exp, repeated); + case GRAPH -> graphReason(exp, repeated); + case OPTIONAL -> optionalReason(exp, repeated); + case EDGE -> exp.getEdge() instanceof PropertyPathEdge ? FallbackReason.PROPERTY_PATH : FallbackReason.NONE; + case QUERY -> FallbackReason.SUBQUERY; + case SERVICE -> FallbackReason.SERVICE; + case MINUS -> FallbackReason.MINUS; + case FILTER -> exp.isPostpone() ? FallbackReason.COMPLEX_OPTIONAL : FallbackReason.NONE; + case BIND -> exp.isFunctional() ? FallbackReason.UNSUPPORTED_OPERATOR : FallbackReason.NONE; + case EMPTY, VALUES -> FallbackReason.NONE; + default -> FallbackReason.UNSUPPORTED_OPERATOR; + }; + } + + private static FallbackReason sequenceReason(Exp exp, boolean repeated) { + boolean inputRepeats = repeated; + for (Exp child : exp) { + FallbackReason reason = fallbackReason(child, inputRepeats); + if (reason != FallbackReason.NONE) return reason; + inputRepeats |= !singleton(child); + } + return FallbackReason.NONE; + } + + private static FallbackReason unionReason(Exp exp, boolean repeated) { + for (Exp branch : exp) { + if (repeated && !tripleJoin(branch) && !singleton(branch)) return FallbackReason.REPEATED_SCOPE; + FallbackReason reason = fallbackReason(branch, false); + if (reason != FallbackReason.NONE) return reason; + } + return FallbackReason.NONE; + } + + private static FallbackReason joinReason(Exp exp, boolean repeated) { + if (tripleJoin(exp)) return FallbackReason.NONE; + if (!singleton(exp.first())) return FallbackReason.SCOPED_JOIN; + FallbackReason left = fallbackReason(exp.first(), repeated); + return left == FallbackReason.NONE ? fallbackReason(exp.rest(), repeated) : left; + } + + private static FallbackReason graphReason(Exp exp, boolean repeated) { + if (repeated && !tripleJoin(exp.get(1)) && !singleton(exp.get(1))) return FallbackReason.REPEATED_SCOPE; + return fallbackReason(exp.get(1), false); + } + + private static FallbackReason optionalReason(Exp exp, boolean repeated) { + if (exp.isPostpone()) return FallbackReason.COMPLEX_OPTIONAL; + FallbackReason left = fallbackReason(exp.first(), repeated); + if (left != FallbackReason.NONE) return left; + return tripleJoin(exp.rest()) ? FallbackReason.NONE : FallbackReason.COMPLEX_OPTIONAL; + } + + /** Avoid repeated independent scans where the existing evaluator uses a buffered join. */ + private static boolean singleton(Exp exp) { + return switch (exp.type()) { + case AND, BGP, JOIN -> exp.getExpList().stream().allMatch(SelectCursor::singleton); + case EMPTY, FILTER, BIND -> true; + case VALUES -> exp.getMappings().size() <= 1; + default -> false; + }; + } + + private static boolean tripleJoin(Exp exp) { + return switch (exp.type()) { + case AND, BGP, JOIN -> exp.getExpList().stream().allMatch(SelectCursor::tripleJoin); + case EDGE -> !(exp.getEdge() instanceof PropertyPathEdge); + case EMPTY -> true; + default -> false; + }; + } + + /** + * Initializes one independent execution without requesting any candidate edge. + * + * @param eval fresh evaluator owned by this execution + * @param query completed eligible SELECT + * @param bindings initial parameter bindings, or null + */ + public SelectCursor(Eval eval, Query query, Mapping bindings) { + this(null, eval, query, bindings, query.getBody(), null, true); + } + + /** + * Opens eligible ORDER BY input without applying OFFSET or LIMIT before ranking. + * Projection and DISTINCT still run for every candidate, using the ordinary evaluator. + * The caller owns both this cursor and its producer's stable read view. + * + * @param eval fresh evaluator owned by this execution + * @param query completed query accepted by {@link #supportsOrderedInput(Query)} + * @param bindings initial parameter bindings, or null + * @return unsliced input cursor + */ + public static SelectCursor orderedInput(Eval eval, Query query, Mapping bindings) { + return new SelectCursor(null, eval, query, bindings, query.getBody(), null, false); + } + + private SelectCursor(SelectCursor parent, Eval eval, Query query, Mapping bindings, Exp body, Node graph, + boolean applySlice) { + this.eval = eval; + this.query = query; + this.scopeGraph = graph; + this.applySlice = applySlice; + parameters = bindings; + nested = parent != null; + cancellation = nested ? parent.cancellation : eval; + producer = eval.getProducer(); + eval.setSubEval(nested); + eval.init(query); + memory = eval.getMemory(); + eval.share(bindings); + if (!nested) { + producer.start(query); + eval.getVisitor().init(query); + eval.getVisitor().before(query); + } + if (bindings != null) eval.bind(bindings); + if (!nested && query.getValues() != null) append(query.getValues()); + append(body); + frames = new Frame[instructions.size()]; + } + + private void append(Exp exp) { + append(instructions, exp); + } + + private static void append(List target, Exp exp) { + if (exp.isFail()) { + target.add(exp); + } else if (exp.isAnd() || exp.isBGP() || exp.type() == Type.JOIN && tripleJoin(exp)) { + for (Exp child : exp) append(target, child); + } else { + target.add(exp); + } + } + + @Override + public boolean hasNext() { + if (next != null) return true; + if (closed) return false; + if (query.isFail() || emitted >= limit()) { + close(); + return false; + } + while (depth >= 0 && !cancellation.isStop()) { + checkInterrupted(); + if (depth == frames.length) { + depth--; + if (selectRow()) return true; + } else if (advance()) { + depth++; + } else { + frames[depth].close(); + frames[depth--] = null; + } + } + close(); + return false; + } + + private static void checkInterrupted() { + if (Thread.currentThread().isInterrupted()) throw new QueryEvaluationException("Query interrupted"); + } + + private boolean selectRow() { + Mapping row = memory.store(query, producer, nested); + if (row == null || !nested && !eval.acceptable(row) + || accepted++ < (nested || !applySlice ? 0 : query.getOffset())) return false; + if (++emitted >= limit()) close(); + next = row; + return true; + } + + /** Requests cooperative cancellation without accessing the owner's binding frames. */ + public void cancel() { + cancellation.finish(); + } + + private long limit() { + return nested || !applySlice ? Long.MAX_VALUE : query.getLimit(); + } + + private boolean advance() { + if (frames[depth] == null) frames[depth] = frame(instructions.get(depth), scopeGraph, depth); + return frames[depth].advance(); + } + + private Frame frame(Exp exp, Node graph, int index) { + if (exp.isFail()) return new OnceFrame(exp, graph, index, false); + return switch (exp.type()) { + case EDGE -> new EdgeFrame(exp, graph, index); + case VALUES -> new ValuesFrame(exp, index); + case JOIN -> new JoinFrame(exp, graph, index); + case UNION -> new UnionFrame(exp, graph, index); + case GRAPH -> new GraphFrame(exp, index); + case OPTIONAL -> new OptionalFrame(exp, graph, index); + default -> new OnceFrame(exp, graph, index, true); + }; + } + + @Override + public Mapping next() { + if (!hasNext()) throw new NoSuchElementException(); + Mapping row = next; + next = null; + return row; + } + + /** @return whether the limit was reached and the final buffered row consumed */ + public boolean isFinished() { + return closed && next == null; + } + + /** Releases active frames from the innermost binding scope outward. Idempotent. */ + @Override + public void close() { + next = null; + if (closed) return; + closed = true; + try { + for (int i = frames.length - 1; i >= 0; i--) { + if (frames[i] != null) { + frames[i].close(); + frames[i] = null; + } + } + } finally { + try { + eval.getEvaluator().finish(memory); + } finally { + if (!nested) producer.finish(query); + } + } + } + + private interface Frame extends AutoCloseable { + boolean advance(); + @Override void close(); + } + + /** Pure triples can reuse bindings; lexical expression scopes need fresh memory. */ + private Frame scope(Exp body, Node graph, int index) { + return tripleJoin(body) ? new Pipeline(body, graph, index) : new ScopedFrame(body, graph, index); + } + + /** Evaluates a lexical group independently, then joins each local row into its caller. */ + private final class ScopedFrame implements Frame { + private final SelectCursor child; + private final int index; + private Mapping bound; + + private ScopedFrame(Exp body, Node graph, int index) { + this.index = index; + Eval evaluator = Eval.create(producer, eval.getEvaluator(), eval.getMatcher()); + child = new SelectCursor(SelectCursor.this, evaluator, query, parameters, body, graph, true); + } + + @Override + public boolean advance() { + release(); + while (child.hasNext()) { + Mapping row = child.next(); + if (memory.push(row, index)) { + bound = row; + return true; + } + } + return false; + } + + private void release() { + if (bound != null) { + memory.pop(bound); + bound = null; + } + } + + @Override + public void close() { + release(); + child.close(); + } + } + + private final class JoinFrame implements Frame { + private final Exp exp; + private final Node graph; + private final int index; + private final Frame left; + private Frame right; + + private JoinFrame(Exp exp, Node graph, int index) { + this.exp = exp; + this.graph = graph; + this.index = index; + left = scope(exp.first(), graph, index); + } + + @Override + public boolean advance() { + if (right != null && right.advance()) return true; + releaseRight(); + while (left.advance() && !cancellation.isStop()) { + right = scope(exp.rest(), graph, index + 1); + if (right.advance()) return true; + releaseRight(); + } + return false; + } + + private void releaseRight() { + if (right != null) { + right.close(); + right = null; + } + } + + @Override + public void close() { + releaseRight(); + left.close(); + } + } + + /** A resumable binding scope; closing it restores its input environment. */ + private final class Pipeline implements Frame { + private final List steps = new ArrayList<>(); + private final Frame[] active; + private final Node graph; + private final int base; + private int position; + private boolean yielded; + + private Pipeline(Exp exp, Node graph, int base) { + this.graph = graph; + this.base = base; + append(steps, exp); + active = new Frame[steps.size()]; + } + + @Override + public boolean advance() { + if (yielded) { + position--; + yielded = false; + } + while (position >= 0 && !cancellation.isStop()) { + checkInterrupted(); + if (position == active.length) { + yielded = true; + return true; + } + if (active[position] == null) active[position] = frame(steps.get(position), graph, base + position); + if (active[position].advance()) { + position++; + } else { + active[position].close(); + active[position--] = null; + } + } + return false; + } + + @Override + public void close() { + for (int i = active.length - 1; i >= 0; i--) { + if (active[i] != null) { + active[i].close(); + active[i] = null; + } + } + position = -1; + } + } + + private final class UnionFrame implements Frame { + private final Exp exp; + private final Node graph; + private final int index; + private int branch; + private Frame current; + + private UnionFrame(Exp exp, Node graph, int index) { + this.exp = exp; + this.graph = graph; + this.index = index; + } + + @Override + public boolean advance() { + while (!cancellation.isStop()) { + if (current != null && current.advance()) return true; + close(); + if (branch == exp.size()) return false; + current = scope(exp.get(branch++), graph, index); + } + return false; + } + + @Override + public void close() { + if (current != null) { + current.close(); + current = null; + } + } + } + + private final class OptionalFrame implements Frame { + private final Exp exp; + private final Node graph; + private final int index; + private final Frame left; + private Frame right; + private boolean matched; + + private OptionalFrame(Exp exp, Node graph, int index) { + this.exp = exp; + this.graph = graph; + this.index = index; + left = scope(exp.first(), graph, index); + } + + @Override + public boolean advance() { + while (!cancellation.isStop()) { + if (right != null) { + if (right.advance()) { + matched = true; + return true; + } + right.close(); + right = null; + if (!matched) return unmatched(); + } + if (!left.advance()) return false; + matched = false; + right = scope(exp.rest(), graph, index); + } + return false; + } + + private boolean unmatched() { + matched = true; + return true; + } + + @Override + public void close() { + if (right != null) right.close(); + right = null; + left.close(); + } + } + + private final class GraphFrame implements Frame { + private final Exp body; + private final Node name; + private final int index; + private final Iterator graphs; + private Frame current; + private Node selected; + private boolean bound; + + private GraphFrame(Exp exp, int index) { + body = exp.get(1); + name = exp.getGraphName(); + this.index = index; + graphs = producer.getGraphNodes(name, query.getFrom(name), memory).iterator(); + } + + @Override + public boolean advance() { + releaseName(); + while (!cancellation.isStop()) { + if (advanceBody()) return true; + close(); + if (!graphs.hasNext()) return false; + selected = graphs.next(); + if (eval.getMatcher().match(name, selected, memory)) { + current = scope(body, selected, index + 1); + } + } + return false; + } + + private boolean advanceBody() { + while (current != null && current.advance()) { + if (memory.push(name, selected, index)) { + bound = true; + return true; + } + } + return false; + } + + private void releaseName() { + if (bound) { + memory.pop(name); + bound = false; + } + } + + @Override + public void close() { + releaseName(); + if (current != null) { + current.close(); + current = null; + } + } + } + + private final class EdgeFrame implements Frame { + private final Edge pattern; + private final int index; + private final Iterable source; + private final Iterator candidates; + private boolean bound; + + private EdgeFrame(Exp exp, Node graph, int index) { + this.index = index; + pattern = exp.getEdge(); + memory.setExp(exp); + source = producer.getEdges(graph, query.getFrom(graph), pattern, memory); + candidates = source.iterator(); + } + + @Override + public boolean advance() { + release(); + while (candidates.hasNext()) { + Edge candidate = candidates.next(); + if (candidate != null && eval.getMatcher().match(pattern, candidate, memory) + && (!pattern.isMatchArity() || pattern.nbNode() == candidate.nbNode()) + && memory.push(producer, pattern, candidate, index)) { + bound = true; + return true; + } + } + return false; + } + + private void release() { + if (bound) { + memory.pop(pattern); + bound = false; + } + } + + @Override + public void close() { + release(); + producer.closeEdges(source); + } + } + + private final class ValuesFrame implements Frame { + private final Exp exp; + private final Iterator rows; + private final int index; + private Mapping bound; + + private ValuesFrame(Exp exp, int index) { + this.exp = exp; + this.index = index; + rows = exp.getMappings().iterator(); + } + + @Override + public boolean advance() { + close(); + while (rows.hasNext()) { + checkInterrupted(); + Mapping row = rows.next(); + if (eval.valuesBinding(exp.getNodeList(), row, index)) { + bound = row; + return true; + } + } + return false; + } + + @Override + public void close() { + if (bound != null) { + eval.free(exp.getNodeList(), bound); + bound = null; + } + } + } + + private final class OnceFrame implements Frame { + private final Exp exp; + private final boolean enabled; + private final Node graph; + private final int index; + private boolean used; + private boolean bound; + + private OnceFrame(Exp exp, Node graph, int index, boolean enabled) { + this.exp = exp; + this.graph = graph; + this.index = index; + this.enabled = enabled; + } + + @Override + public boolean advance() { + close(); + if (used || !enabled) return false; + used = true; + return switch (exp.type()) { + case FILTER -> eval.test(graph, exp.getFilter(), memory, producer); + case BIND -> bind(); + default -> true; + }; + } + + private boolean bind() { + Node value; + try { + value = eval.eval(graph, exp.getFilter(), memory, producer); + } catch (QueryTypeErrorException error) { + return true; + } + if (value == null) return true; + bound = memory.push(exp.getNode(), value, index); + return bound; + } + + @Override + public void close() { + if (bound) { + memory.pop(exp.getNode()); + bound = false; + } + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/SparqlException.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/SparqlException.java deleted file mode 100644 index d54f140a1..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/SparqlException.java +++ /dev/null @@ -1,28 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.eval; - -/** - * - * @author corby - */ -public class SparqlException extends Exception { - private static final long serialVersionUID = 1L; - - private final boolean stop; - - public SparqlException() { - this(false); - } - - public SparqlException(boolean stop) { - this.stop = stop; - } - - // isStop true means stop query processing, perform aggregate etc. and return partial result - // isStop false means this is an exception - // see LDScriptException in sparql - - public boolean isStop() { - return stop; - } - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/Stack.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/Stack.java index 9acf9130e..6e8b59113 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/Stack.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/eval/Stack.java @@ -13,36 +13,39 @@ * @author Olivier Corby, Edelweiss, INRIA 2009 */ public class Stack extends ArrayList implements ExpType { + @java.io.Serial + private static final long serialVersionUID = 1L; - public static Stack create(Exp e){ - Stack st = new Stack(); - st.add(e); - return st; - } + public static Stack create(Exp e){ + Stack st = new Stack(); + st.add(e); + return st; + } - Stack copy(){ - Stack st = new Stack(); - st.addAll(this); - return st; - } + Stack copy(){ + Stack st = new Stack(); + st.addAll(this); + return st; + } - /** - * Push all elements of AND in the stack - */ - Stack and(Exp exp, int n){ - remove(n); - int i = 0; - for (Exp e : exp){ + + /** + * Push all elements of AND in the stack + */ + Stack and(Exp exp, int n){ + remove(n); + int i = 0; + for (Exp e : exp){ if (e.getBind() != null){ add(n + i++, e.getBind()); } add(n + i++, e); - } - return this; - } + } + return this; + } Stack addCopy(int n, Exp exp) { Stack copy = copy(); @@ -52,13 +55,13 @@ Stack addCopy(int n, Exp exp) { @Override - public String toString(){ - StringBuilder str = new StringBuilder(); - int i = 0; - for (Exp e : this){ - str.append(i++).append(" ").append(e).append(", "); - } - return str.toString(); - } + public String toString(){ + StringBuilder str = new StringBuilder(); + int i = 0; + for (Exp e : this){ + str.append(i++).append(" ").append(e).append(", "); + } + return str.toString(); + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/Event.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/Event.java index e167adfc4..c3767de39 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/Event.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/Event.java @@ -10,70 +10,70 @@ */ public interface Event { - int BEGIN = 0; - int START = 1; - int ENUM = 2; - int MATCH = 3; - int FILTER = 4; - int BIND = 5; - int GRAPH = 6; - int PATH = 7; - int PATHSTEP = 8; + int BEGIN = 0; + int START = 1; + int ENUM = 2; + int MATCH = 3; + int FILTER = 4; + int BIND = 5; + int GRAPH = 6; + int PATH = 7; + int PATHSTEP = 8; - int AGG = 13; + int AGG = 13; - int FINISH = 14; - int DISTINCT = 15; - int LIMIT = 16; + int FINISH = 14; + int DISTINCT = 15; + int LIMIT = 16; - int RESULT = 19; - int END = 20; + int RESULT = 19; + int END = 20; - int ALL = 50; + int ALL = 50; - // events from user to listener + // events from user to listener - // next step + // next step int STEP = 101; - // next edge/node/path + // next edge/node/path int NEXT = 102; - // skip trace until next expression in stack + // skip trace until next expression in stack int FORWARD = 103; - // eval current expression silently + // eval current expression silently int COMPLETE = 104; - // eval until current expression succeed + // eval until current expression succeed int SUCCESS = 105; - // resume execution silently + // resume execution silently int QUIT = 109; - // pprint current Mapping + // pprint current Mapping int MAP = 110; - int VERBOSE = 111; - int HELP = 112; + int VERBOSE = 111; + int HELP = 112; - int getSort(); + int getSort(); - Object getObject(); + Object getObject(); - Object getArg(int n); + Object getArg(int n); - Exp getExp(); + Exp getExp(); - boolean isSuccess(); + boolean isSuccess(); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/EventImpl.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/EventImpl.java index f61afe6ee..0fc339508 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/EventImpl.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/EventImpl.java @@ -12,107 +12,107 @@ public class EventImpl implements Event { - int type; - boolean isSuccess = true; - Object object; - Object arg; - Object arg2; - - EventImpl(int n, Object o){ - type = n; - object = o; - } - - EventImpl(int n, Object o, Object o2){ - type = n; - object = o; - arg = o2; - if (o2 instanceof Boolean success){ - isSuccess = success; - } - } - - EventImpl(int n, Object o, Object o2, Object o3){ - type = n; - object = o; - arg = o2; - arg2 = o3; - if (o3 instanceof Boolean success){ - isSuccess = success; - } - } - - public String toString(){ - String str = getTitle(); - if (object != null){ - str += " "; - if (object instanceof Exp exp && exp.isEdge()){ - str += "("+ exp.getEdge().getEdgeIndex() + ") "; - } - str += object; - } - if (arg != null){ - str += "\n"; - str += arg; - } - if (arg2 != null && ! (arg2 instanceof Stack)) str += "\n" + arg2; - - return str; - } - - - String getTitle(){ - return switch (type) { - case BEGIN -> "begin"; - case START -> "start"; - case ENUM -> "enum"; - case FILTER -> "filter"; - case BIND -> "bind"; - case MATCH -> "match"; - case GRAPH -> "graph"; - case PATH -> "path"; - case PATHSTEP, STEP -> "step"; - case FINISH -> "finish"; - case AGG -> "aggregate"; - case DISTINCT -> "distinct"; - case LIMIT -> "limit"; - case RESULT -> "result"; - case END -> "end"; - case COMPLETE -> "complete"; - case FORWARD -> "forward"; - case MAP -> "map"; - case NEXT -> "next"; - case QUIT -> "quit"; - case SUCCESS -> "success"; - case VERBOSE -> "verbose"; - case HELP -> "help"; - default -> "event(" + type + ')'; - }; - } - - - - public static EventImpl create(int type, Object obj){ + int type; + boolean isSuccess = true; + Object object; + Object arg; + Object arg2; + + EventImpl(int n, Object o){ + type = n; + object = o; + } + + EventImpl(int n, Object o, Object o2){ + type = n; + object = o; + arg = o2; + if (o2 instanceof Boolean success){ + isSuccess = success; + } + } + + EventImpl(int n, Object o, Object o2, Object o3){ + type = n; + object = o; + arg = o2; + arg2 = o3; + if (o3 instanceof Boolean success){ + isSuccess = success; + } + } + + public String toString(){ + String str = getTitle(); + if (object != null){ + str += " "; + if (object instanceof Exp exp && exp.isEdge()){ + str += "("+ exp.getEdge().getEdgeIndex() + ") "; + } + str += object; + } + if (arg != null){ + str += "\n"; + str += arg; + } + if (arg2 != null && ! (arg2 instanceof Stack)) str += "\n" + arg2; + + return str; + } + + + String getTitle(){ + return switch (type) { + case BEGIN -> "begin"; + case START -> "start"; + case ENUM -> "enum"; + case FILTER -> "filter"; + case BIND -> "bind"; + case MATCH -> "match"; + case GRAPH -> "graph"; + case PATH -> "path"; + case PATHSTEP, STEP -> "step"; + case FINISH -> "finish"; + case AGG -> "aggregate"; + case DISTINCT -> "distinct"; + case LIMIT -> "limit"; + case RESULT -> "result"; + case END -> "end"; + case COMPLETE -> "complete"; + case FORWARD -> "forward"; + case MAP -> "map"; + case NEXT -> "next"; + case QUIT -> "quit"; + case SUCCESS -> "success"; + case VERBOSE -> "verbose"; + case HELP -> "help"; + default -> "event(" + type + ')'; + }; + } + + + + public static EventImpl create(int type, Object obj){ return new EventImpl(type, obj); - } + } - public static EventImpl create(int type, Object obj, Object arg){ + public static EventImpl create(int type, Object obj, Object arg){ return new EventImpl(type, obj, arg); - } + } - public static EventImpl create(int type, Object obj, Object arg, Object arg2){ + public static EventImpl create(int type, Object obj, Object arg, Object arg2){ return new EventImpl(type, obj, arg, arg2); - } + } - public int getSort(){ - return type; - } + public int getSort(){ + return type; + } - public Object getObject(){ - return object; - } + public Object getObject(){ + return object; + } - public Object getArg(int n){ + public Object getArg(int n){ return switch (n) { case 0 -> object; case 1 -> arg; @@ -121,15 +121,15 @@ public Object getArg(int n){ }; } - public Exp getExp(){ - if (object instanceof Exp expression){ - return expression; - } - return null; - } + public Exp getExp(){ + if (object instanceof Exp expression){ + return expression; + } + return null; + } - public boolean isSuccess(){ - return isSuccess; - } + public boolean isSuccess(){ + return isSuccess; + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/EventListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/EventListener.java index 47bb6d597..97bcd1e7d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/EventListener.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/EventListener.java @@ -8,13 +8,13 @@ */ public interface EventListener { - boolean send(Event e); + boolean send(Event e); - boolean handle(int sort); + boolean handle(int sort); - boolean handle(int sort, boolean b); + boolean handle(int sort, boolean b); - void setObject(Object obj); + void setObject(Object obj); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/KgramEventDispatcher.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/KgramEventDispatcher.java index c3ab90590..c66473378 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/KgramEventDispatcher.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/KgramEventDispatcher.java @@ -12,7 +12,6 @@ */ public class KgramEventDispatcher implements Iterable { - boolean isEval = false; List observers = new ArrayList<>(); 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 index 7e467757c..95b7956bf 100644 --- 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 @@ -2,7 +2,6 @@ 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.Regex; 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; @@ -15,24 +14,21 @@ */ 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 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); + /** + * 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 enter(Edge ent, Regex exp, int size); - - boolean leave(Edge ent, Regex exp, int size); boolean listen(Exp exp, Edge query, Edge target); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/User.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/User.java deleted file mode 100644 index f51b492b8..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/event/User.java +++ /dev/null @@ -1,16 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.event; - -/** - * Model user interaction with debugger - * - * @author Olivier Corby, Edelweiss, INRIA 2010 - * - */ -public interface User { - - int get(); - - String help(); - - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/Checker.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/Checker.java deleted file mode 100644 index 9b94d7d0f..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/Checker.java +++ /dev/null @@ -1,216 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.filter; - -import fr.inria.corese.core.next.query.impl.engine.model.Expr; -import fr.inria.corese.core.next.query.impl.engine.model.ExprType; - -import java.util.ArrayList; -import java.util.List; - - -/** - * Filter Exp Checker - * Check presence of patterns that are always true or always false - * Such as: - * ?x != ?x - * ?x > ?y && ?x < ?y - * - * @author Olivier Corby, Edelweiss, INRIA 2010 - * - */ -public final class Checker implements ExprType { - static final int BOOL = BOOLEAN; - - private final List alwaysFalse; - private final Matcher matcher; - - public Checker(){ - matcher = new Matcher(); - alwaysFalse = createAlwaysFalsePatterns(); - } - - /** - * Check one filter - */ - public boolean check(Expr ee){ - // match = true means that a false pattern matches - // hence return false (check correctness is false) - return !match(ee); - } - - - /** - * Return true if a false pattern matches - * return false otherwise - */ - boolean match(Expr ee){ - - - return match(ee, alwaysFalse); - } - - - boolean match(Expr ee, List pat) { - for (FilterPattern p : pat){ - if (matcher.match(p, ee)){ - return true; - } - } - return false; - } - - - - /************************************************* - * Always false patterns - * - */ - - List createAlwaysFalsePatterns(){ - List patterns = new ArrayList<>(); - patterns.add(neqSelf()); - patterns.add(ltSelf()); - patterns.add(notEqSelf()); - patterns.add(notGeSelf()); - patterns.add(patNotPat()); - patterns.add(notOr()); - patterns.add(eqNeq()); - patterns.add(eqGt()); - patterns.add(ltGt()); - patterns.add(gtNotGe()); - patterns.add(eqNotGe()); - return List.copyOf(patterns); - } - - - /** - * ?from=cst || ?to=cst - */ - FilterPattern path(String v1, String v2){ - FilterPattern e1 = term(EQ, FilterPattern.variable(v1), constant()); - FilterPattern e2 = term(EQ, FilterPattern.variable(v2), constant()); - return or(e1, e2); - } - - // always false - FilterPattern neqSelf(){ - // EXP != EXP - FilterPattern exp = pat(); - return term(NE, exp, exp); - - } - - FilterPattern ltSelf(){ - // EXP < EXP - FilterPattern exp = pat(); - return term(LT, exp, exp); - - } - - FilterPattern notEqSelf(){ - // !(EXP = EXP) - FilterPattern exp = pat(); - return not(term(EQ, exp, exp)); - - } - - FilterPattern notGeSelf(){ - // !(EXP >= EXP) - FilterPattern exp = pat(); - return not(term(GE, exp, exp)); - - } - - - - FilterPattern patNotPat(){ - // EXP && ! EXP - FilterPattern exp = pat(); - return and(exp, not(exp)); - } - - - FilterPattern notOr(){ - // ! (EXP || ! EXP) - FilterPattern exp = pat(); - return not(or(exp, not(exp))); - } - - - // Note: - // we can have both with list of values - // ?x = xpath() && ?x != xpath() - FilterPattern eqNeq(){ - // EXP1 = EXP2 && EXP1 != EXP2 - FilterPattern e1 = pat(); - FilterPattern e2 = pat(); - return and(term(EQ, e1, e2), term(NE, e1, e2)); - } - - - FilterPattern eqGt(){ - // EXP1 = EXP2 && EXP1 > EXP2 - FilterPattern e1 = pat(); - FilterPattern e2 = pat(); - return and(term(EQ, e1, e2), term(GT, e1, e2)); - } - - FilterPattern ltGt(){ - // EXP1 > EXP2 && EXP1 < EXP2 - FilterPattern e1 = pat(); - FilterPattern e2 = pat(); - return and(term(GT, e1, e2), term(LT, e1, e2)); - } - - - FilterPattern gtNotGe(){ - // EXP1 > EXP2 && ! (EXP1 >= EXP2) - FilterPattern e1 = pat(); - FilterPattern e2 = pat(); - return and(term(GT, e1, e2), not(term(GE, e1, e2))); - } - - FilterPattern eqNotGe(){ - // EXP1 = EXP2 && ! (EXP1 >= EXP2) - FilterPattern e1 = pat(); - FilterPattern e2 = pat(); - return and(term(EQ, e1, e2), not(term(GE, e1, e2))); - } - - - FilterPattern constant(){ - return FilterPattern.constant(); - } - - - FilterPattern pat() { - return new FilterPattern(ExprType.JOKER); - } - - FilterPattern pat(FilterPattern e1) { - return new FilterPattern(ExprType.BOOLEAN, ExprType.NOT, e1); - } - - FilterPattern pat(int type, int ope, FilterPattern e1, FilterPattern e2){ - return new FilterPattern(type, ope, e1, e2); - } - - FilterPattern not(FilterPattern e){ - return pat(e); - } - - FilterPattern and(FilterPattern e1, FilterPattern e2){ - return pat(BOOLEAN, AND, e1, e2); - } - - FilterPattern or(FilterPattern e1, FilterPattern e2){ - return pat(BOOLEAN, OR, e1, e2); - } - - FilterPattern term(int ope, FilterPattern e1, FilterPattern e2){ - return pat(TERM, ope, e1, e2); - } - - - - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/Compile.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/Compile.java deleted file mode 100644 index 8dc8e7d4d..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/Compile.java +++ /dev/null @@ -1,254 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.filter; - -import fr.inria.corese.core.next.query.impl.engine.model.ExpType; -import fr.inria.corese.core.next.query.impl.engine.model.Expr; -import fr.inria.corese.core.next.query.impl.engine.model.ExprType; -import fr.inria.corese.core.next.query.impl.engine.model.Filter; -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.pattern.Query; - -import java.util.ArrayList; -import java.util.List; - -/** - * Filter Exp Compiler for Optimizations - * If Filter leads to optimization, we tag the Filter Exp - * with e.g. a BIND(?x = ?y) - * Query.bind() will handle later this tag BIND - * (1) - * x p t . y p z . filter(y = x) - * -> - * x p t . bind(y, x) . y p z - * (2) - * x p t filter(?x = ) - * -> - * bind(x, ) . x p t - * (3) - * x p t filter(?x = || ?x = ) - * -> - * bind(x, (, )) . x p t - * (4) - * optional{} !bound(?x) - * !bound() backjump before optional{} - * if there is no optional, !bound() backjump to edge before where ?x is bound - * - * @author Olivier Corby, Edelweiss, INRIA 2010 - * - */ -public class Compile implements ExprType { - - Query kgramQuery; - Matcher matcher; - Checker checker; - - public Compile(Query q){ - kgramQuery = q; - matcher = new Matcher(); - checker = new Checker(); - } - - /** - * exp is a FILTER Exp - * when !bound(?x) : get Node ?x for optimizing backjump with optional - * when ?x = ?y : get list Node, for optimizing edge - * Note: - * assign filter to edge and put edge first in its component: - * x p t . y p z filter(y = x) - * we can bind y = x before enumerate y p z - */ - public void process(Query q, Exp exp) { - this.kgramQuery = q; - Filter ff = exp.getFilter(); - Expr ee = ff.getExp(); - - switch (ee.oper()){ - - case OR: - or(exp); - break; - - case EQ: - eq(exp); - break; - // Note: IN, VALUES - case IN: - in(exp); - break; - default: - if (matcher.isGL(ee.oper())){ - gl(exp); - } - - } - - } - - - - /** - * ?x = ?y - * ?x = cst - */ - void eq(Exp exp){ - Filter ff = exp.getFilter(); - Expr ee = ff.getExp(); - - // ?x = ?y - FilterPattern pat = new FilterPattern(TERM, EQ, VARIABLE, VARIABLE); - if (matcher.match(pat, ee)){ - // compute Node list corresponding to variables - List lNode = kgramQuery.getNodes(exp); - if (lNode.size()==2){ - Exp bind = Exp.create(ExpType.Type.OPT_BIND); - for (Node qNode : lNode){ - Exp variable = Exp.create(ExpType.Type.NODE, qNode); - bind.add(variable); - } - exp.add(bind); - } - } - // ?x = 'constant' - else if (matcher.match(new FilterPattern(TERM, EQ, VARIABLE, CONSTANT), ee)){ - Exp bind = buildCst(exp, ExpType.Type.OPT_BIND); - if (bind != null){ - exp.add(bind); - } - } - } - - - - Exp buildCst(Exp exp, ExpType.Type type){ - Filter ff = exp.getFilter(); - Expr ee = ff.getExp(); - Node node = kgramQuery.getProperAndSubSelectNode(ff.getVariables().getFirst()); - if (node != null){ - // variable ?x - Exp bind = Exp.create(type, Exp.create(ExpType.Type.NODE, node)); - List list = new ArrayList<>(); - list.add(getConstants(ee).getFirst()); - bind.setObject(list); - return bind; - } - return null; - } - - Exp buildVar(Exp exp){ - List lNode = kgramQuery.getNodes(exp); - if (lNode.size()==2){ - Exp bind = Exp.create(ExpType.Type.TEST); - for (Node node : lNode){ - bind.add(Exp.create(ExpType.Type.NODE, node)); - } - return bind; - } - return null; - } - - - - /** - * VAR < CST - */ - void gl(Exp exp){ - Filter ff = exp.getFilter(); - Expr ee = ff.getExp(); - FilterPattern pat = new FilterPattern(TERM, GL, VARIABLE, CONSTANT); - if (matcher.match(pat, ee)){ - Exp test = buildCst(exp, ExpType.Type.TEST); - if (test!=null) - exp.add(test); - } - else { - pat = new FilterPattern(TERM, GL, VARIABLE, VARIABLE); - if (matcher.match(pat, ee)){ - Exp test = buildVar(exp); - if (test!=null) - exp.add(test); - } - } - - } - - /** - * ?x = cst1 || ?x = cst2 - */ - void or(Exp exp){ - Filter ff = exp.getFilter(); - Expr ee = ff.getExp(); - FilterPattern pat = new FilterPattern(BOOLEAN, OR, new FilterPattern(TERM, EQ, VARIABLE, CONSTANT)); - pat.setRec(); - pat.setMatchConstant(); - if (matcher.match(pat, ee)) { - Node node = kgramQuery.getProperAndSubSelectNode(ff.getVariables().getFirst()); - if (node != null){ - List list = getConstants(ee); - Exp bind = Exp.create(ExpType.Type.OPT_BIND, Exp.create(ExpType.Type.NODE, node)); - bind.setObject(list); - exp.add(bind); - } - } - - } - - /* - * ?x IN (2 4) - * only consider the case with all constants (for the moment) - */ - void in(Exp exp) { - Filter ff = exp.getFilter(); - Expr expr = ff.getExp(); - List lvar = ff.getVariables(); - Expr variable = expr.getExp(0); - if (! variable.isVariable()){ - return; - } - List values = expr.getExp(1).getExpList(); - List list = new ArrayList<>(); - //all has to be constants - for (Expr e : values) { - if (e.type() != CONSTANT) { - return; - } - list.add(e); - } - - Node node = kgramQuery.getProperAndSubSelectNode(lvar.getFirst()); - if (node != null) { - Exp bind = Exp.create(ExpType.Type.OPT_BIND, Exp.create(ExpType.Type.NODE, node)); - bind.setObject(list); - exp.add(bind); - } - } - - List getConstants(Expr exp){ - List list = new ArrayList<>(); - return getConstants(exp, list); - } - - - List getConstants(Expr exp, List list){ - switch (exp.type()){ - - case CONSTANT: - list.add(exp); break; - - case VARIABLE: break; - - default: - for (Expr ee : exp.getExpList()){ - getConstants(ee, list); - } - } - return list; - } - - - - public boolean isLocal(Filter f){ - return f.isBound() || - f.getExp().oper() == DEBUG; - } -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/ExpressionPatternMatcher.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/ExpressionPatternMatcher.java new file mode 100644 index 000000000..0e1775d9f --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/ExpressionPatternMatcher.java @@ -0,0 +1,230 @@ +package fr.inria.corese.core.next.query.impl.engine.filter; + +import fr.inria.corese.core.next.query.impl.engine.model.Expr; +import fr.inria.corese.core.next.query.impl.engine.model.ExprType; + + +/** + * Filter Exp ExpressionPatternMatcher + * + * @author Olivier Corby, Edelweiss, INRIA 2010 + * + */ +final class ExpressionPatternMatcher implements ExprType { + boolean rec = false; + boolean matchConstant = true; + + + public boolean match(FilterPattern qe, Expr te){ + MatchBind bind = MatchBind.create(); + return match(qe, te, bind); + } + + boolean match(FilterPattern qe, Expr te, MatchBind bind){ + rec = qe.isRec(); + matchConstant = qe.isMatchConstant(); + return process(qe, te, bind); + } + + + + + /** + * Expr matcher + * Special case: + * OR(EXP) with rec=true + * means match OR(EXP) recursively with OR(EXP1, OR(EXP2, EXP3)) + * EXP is matched with all EXPi + * Pattern variable store target variable + * Afterwards, Pattern variable must match same target variables in all EXPi + * + */ + boolean process(Expr qe, Expr te, MatchBind bind) { + if (qe.type() == ALTER) { + return matchAlternative(qe, te, bind); + } + if (!matchType(qe, te)) { + return false; + } + if ((qe.type() != CONSTANT || matchConstant) && bind.hasValue(qe)) { + return process(bind.getValue(qe), te, bind); + } + return switch (qe.type()) { + case VARIABLE, CONSTANT -> matchVarConst(qe, te, bind); + case FUNCTION, JOKER -> matchBoolFunAny(qe, te, bind); + case BOOLEAN -> matchBoolean(qe, te, bind); + case TERM -> matchTerm(qe, te, bind); + default -> false; + }; + } + + private boolean matchAlternative(Expr qe, Expr te, MatchBind bind) { + for (Expr alternative : qe.getExpList()) { + int size = bind.size(); + if (process(alternative, te, bind)) { + return true; + } + bind.clean(size); + } + return false; + } + + private boolean matchBoolean(Expr qe, Expr te, MatchBind bind) { + if (qe.arity() == 2 && te.arity() == 2) { + return matchTerm(qe, te, bind); + } + return qe.arity() <= te.arity() && matchBoolFunAny(qe, te, bind); + } + + boolean matchVarConst(Expr qe, Expr te, MatchBind bind) { + if (qe.type() == VARIABLE) { + if (qe.getLabel() != null) { + return qe.getLabel().equals(te.getLabel()); + } + bind.setValue(qe, te); + } else if (qe.type() == CONSTANT && matchConstant) { + if (!isBindable(qe)) { + return qe.equals(te); + } + bind.setValue(qe, te); + } + return true; + } + + + /** + * BOOLEAN NOT + * BOOLEAN with rec = true + * OR(EXP) matches recursively OR(OR(EXP1, EXP2), EXP3) + * FUNCTION ANY + */ + boolean matchBoolFunAny(Expr qe, Expr te, MatchBind bind) { + if (qe.arity() == 0) { + // qe e.g. Pattern(ANY, ANY) with no args + // first time: bind the pattern + bind.setValue(qe, te); + return true; + } + if (te.arity() == 0) { + // qe has arity, te has not: fail + return false; + } + + int size = bind.size(); + int i = 0; + Expr qarg; + + for (Expr targ : te.getExpList()) { + if (i < qe.arity()) { + // use pattern ith arg + qarg = qe.getExp(i++); + } else if (rec) { + // reuse pattern 0th arg + qarg = qe.getExp(0); + } else { + // query has less arguments than target + // and query args match target args + // should reset bindings here + bind.clean(size); + return false; + } + + if (!process(qarg, targ, bind) && (!rec || !process(qe, targ, bind))) { + bind.clean(size); + return false; + } + } + + if (!bind.hasValue(qe)) { + // because when rec = true, qe may have already been bound to an arg of te + bind.setValue(qe, te); + } + return true; + } + + + boolean matchTerm(Expr qe, Expr te, MatchBind bind) { + Expr fst = qe.getExp(0); + Expr snd = qe.getExp(1); + + if (isGL(qe.oper()) && qe.oper() != te.oper()) { + + if (inverse(qe.oper(), te.oper())) { + // qe(x < y) VS te(y > x) + // try switch qe args + fst = qe.getExp(1); + snd = qe.getExp(0); + } + else return false; + } + + int size = bind.size(); + boolean b = + process(fst, te.getExp(0), bind) && + process(snd, te.getExp(1), bind); + + if (! b){ + bind.clean(size); + + if (symmetric(qe)){ + // try switch arguments of qe + // switch ?x = ?y to ?y = ?x + b = + process(snd, te.getExp(0), bind) && + process(fst, te.getExp(1), bind); + } + } + + if (b) bind.setValue(qe, te); + return b; + } + + /** + * Is it a Pattern constant + */ + boolean isBindable(Expr exp){ + return exp instanceof FilterPattern; + } + + boolean symmetric(Expr qe){ + if (qe.type() == BOOLEAN && qe.oper() == JOKER) return true; + int t = qe.oper(); + return t == EQ || t == NE || t == PLUS || t == MULT || t == AND || t == OR; + } + + boolean inverse(int t1, int t2){ + return + (t1 == LT && t2 == GT) || + (t1 == GT && t2 == LT) || + (t1 == GE && t2 == LE) || + (t1 == LE && t2 == GE) ; + } + + boolean isGL(int tt){ + return tt == GL || tt == LT || tt == LE || tt == GT || tt == GE; + } + + + /** + * accept comparison operator with any comparison because term matcher + * will permute arguments as needed + */ + boolean match(int qt, int tt){ + if (qt == JOKER) return true; + if (isGL(qt)) return isGL(tt); + if (qt == EQNE) return tt == EQ || tt == NE; + return qt == tt; + } + + boolean matchType(Expr qe, Expr te){ + if (qe.oper() == EXIST || te.oper() == EXIST) return false; + if (qe.type() == JOKER) { + return true; + } + return switch (qe.type()) { + case VARIABLE, CONSTANT -> match(qe.type(), te.type()); + default -> match(qe.type(), te.type()) && match(qe.oper(), te.oper()); + }; + } + +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/FilterOptimizer.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/FilterOptimizer.java new file mode 100644 index 000000000..f8bf10441 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/FilterOptimizer.java @@ -0,0 +1,252 @@ +package fr.inria.corese.core.next.query.impl.engine.filter; + +import fr.inria.corese.core.next.query.impl.engine.model.ExpType; +import fr.inria.corese.core.next.query.impl.engine.model.Expr; +import fr.inria.corese.core.next.query.impl.engine.model.ExprType; +import fr.inria.corese.core.next.query.impl.engine.model.Filter; +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.pattern.Query; + +import java.util.ArrayList; +import java.util.List; + +/** + * Filter Exp Compiler for Optimizations + * If Filter leads to optimization, we tag the Filter Exp + * with e.g. a BIND(?x = ?y) + * Query.bind() will handle later this tag BIND + * (1) + * x p t . y p z . filter(y = x) + * -> + * x p t . bind(y, x) . y p z + * (2) + * x p t filter(?x = ) + * -> + * bind(x, ) . x p t + * (3) + * x p t filter(?x = || ?x = ) + * -> + * bind(x, (, )) . x p t + * (4) + * optional{} !bound(?x) + * !bound() backjump before optional{} + * if there is no optional, !bound() backjump to edge before where ?x is bound + * + * @author Olivier Corby, Edelweiss, INRIA 2010 + * + */ +public final class FilterOptimizer implements ExprType { + + private Query kgramQuery; + private final ExpressionPatternMatcher matcher; + + public FilterOptimizer(Query q){ + kgramQuery = q; + matcher = new ExpressionPatternMatcher(); + } + + /** + * exp is a FILTER Exp + * when !bound(?x) : get Node ?x for optimizing backjump with optional + * when ?x = ?y : get list Node, for optimizing edge + * Note: + * assign filter to edge and put edge first in its component: + * x p t . y p z filter(y = x) + * we can bind y = x before enumerate y p z + */ + public void process(Query q, Exp exp) { + this.kgramQuery = q; + Filter ff = exp.getFilter(); + Expr ee = ff.getExp(); + + switch (ee.oper()){ + + case OR: + or(exp); + break; + + case EQ: + eq(exp); + break; + // Note: IN, VALUES + case IN: + in(exp); + break; + default: + if (matcher.isGL(ee.oper())){ + gl(exp); + } + + } + + } + + + + /** + * ?x = ?y + * ?x = cst + */ + void eq(Exp exp){ + Filter ff = exp.getFilter(); + Expr ee = ff.getExp(); + + // ?x = ?y + FilterPattern pat = new FilterPattern(TERM, EQ, VARIABLE, VARIABLE); + if (matcher.match(pat, ee)){ + // compute Node list corresponding to variables + List lNode = kgramQuery.getNodes(exp); + if (lNode.size()==2){ + Exp bind = Exp.create(ExpType.Type.OPT_BIND); + for (Node qNode : lNode){ + Exp variable = Exp.create(ExpType.Type.NODE, qNode); + bind.add(variable); + } + exp.add(bind); + } + } + // ?x = 'constant' + else if (matcher.match(new FilterPattern(TERM, EQ, VARIABLE, CONSTANT), ee)){ + Exp bind = buildCst(exp, ExpType.Type.OPT_BIND); + if (bind != null){ + exp.add(bind); + } + } + } + + + + Exp buildCst(Exp exp, ExpType.Type type){ + Filter ff = exp.getFilter(); + Expr ee = ff.getExp(); + Node node = kgramQuery.getProperAndSubSelectNode(ff.getVariables().getFirst()); + if (node != null){ + // variable ?x + Exp bind = Exp.create(type, Exp.create(ExpType.Type.NODE, node)); + List list = new ArrayList<>(); + list.add(getConstants(ee).getFirst()); + bind.setObject(list); + return bind; + } + return null; + } + + Exp buildVar(Exp exp){ + List lNode = kgramQuery.getNodes(exp); + if (lNode.size()==2){ + Exp bind = Exp.create(ExpType.Type.TEST); + for (Node node : lNode){ + bind.add(Exp.create(ExpType.Type.NODE, node)); + } + return bind; + } + return null; + } + + + + /** + * VAR < CST + */ + void gl(Exp exp){ + Filter ff = exp.getFilter(); + Expr ee = ff.getExp(); + FilterPattern pat = new FilterPattern(TERM, GL, VARIABLE, CONSTANT); + if (matcher.match(pat, ee)){ + Exp test = buildCst(exp, ExpType.Type.TEST); + if (test!=null) + exp.add(test); + } + else { + pat = new FilterPattern(TERM, GL, VARIABLE, VARIABLE); + if (matcher.match(pat, ee)){ + Exp test = buildVar(exp); + if (test!=null) + exp.add(test); + } + } + + } + + /** + * ?x = cst1 || ?x = cst2 + */ + void or(Exp exp){ + Filter ff = exp.getFilter(); + Expr ee = ff.getExp(); + FilterPattern pat = new FilterPattern(BOOLEAN, OR, new FilterPattern(TERM, EQ, VARIABLE, CONSTANT)); + pat.setRec(); + pat.setMatchConstant(); + if (matcher.match(pat, ee)) { + Node node = kgramQuery.getProperAndSubSelectNode(ff.getVariables().getFirst()); + if (node != null){ + List list = getConstants(ee); + Exp bind = Exp.create(ExpType.Type.OPT_BIND, Exp.create(ExpType.Type.NODE, node)); + bind.setObject(list); + exp.add(bind); + } + } + + } + + /* + * ?x IN (2 4) + * only consider the case with all constants (for the moment) + */ + void in(Exp exp) { + Filter ff = exp.getFilter(); + Expr expr = ff.getExp(); + List lvar = ff.getVariables(); + Expr variable = expr.getExp(0); + if (! variable.isVariable()){ + return; + } + List values = expr.getExp(1).getExpList(); + List list = new ArrayList<>(); + //all has to be constants + for (Expr e : values) { + if (e.type() != CONSTANT) { + return; + } + list.add(e); + } + + Node node = kgramQuery.getProperAndSubSelectNode(lvar.getFirst()); + if (node != null) { + Exp bind = Exp.create(ExpType.Type.OPT_BIND, Exp.create(ExpType.Type.NODE, node)); + bind.setObject(list); + exp.add(bind); + } + } + + List getConstants(Expr exp){ + List list = new ArrayList<>(); + return getConstants(exp, list); + } + + + List getConstants(Expr exp, List list){ + switch (exp.type()){ + + case CONSTANT: + list.add(exp); break; + + case VARIABLE: break; + + default: + for (Expr ee : exp.getExpList()){ + getConstants(ee, list); + } + } + return list; + } + + + + public boolean isLocal(Filter f){ + return f.isBound() || + f.getExp().oper() == DEBUG; + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/FilterPattern.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/FilterPattern.java index d3f23ce8e..5ed1be1a0 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/FilterPattern.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/FilterPattern.java @@ -1,5 +1,8 @@ package fr.inria.corese.core.next.query.impl.engine.filter; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopy; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopyable; + import fr.inria.corese.core.next.query.impl.engine.model.BindingContext; import fr.inria.corese.core.next.query.impl.engine.model.Expr; import fr.inria.corese.core.next.query.impl.engine.model.ExprType; @@ -15,290 +18,182 @@ import java.util.List; /** - * Filter Expression Pattern Matcher. + * Filter Expression Pattern ExpressionPatternMatcher. * * This class provides pattern matching capabilities for filter expressions. * * @author Olivier Corby, Edelweiss, INRIA 2010 */ -public final class FilterPattern implements ExprType, Expr { +public final class FilterPattern implements ExprType, Expr, PlanCopyable { private static final String NOT_SUPPORTED_YET = "Not supported yet."; - int type; - int oper; - String label; - // recursive pattern, ako * - boolean rec = false; - boolean matchConstant = true; - List args; - Expr exp; - - FilterPattern(int t) { - type = t; - oper = JOKER; - args = new ArrayList<>(); - } - - FilterPattern(int t, int o) { - this(t); - oper = o; - } - - FilterPattern(int t, int o, Expr e1) { - this(t, o); - add(e1); - } - - FilterPattern(int t, int o, Expr e1, Expr e2) { - this(t, o, e1); - add(e2); - } - - - public FilterPattern(int t, int o, int e1, int e2){ - this(t, o); - add(new FilterPattern(e1)); - add(new FilterPattern(e2)); - } - - static FilterPattern variable(String label) { - FilterPattern p = new FilterPattern(VARIABLE); - p.setLabel(label); - return p; - } - - static FilterPattern constant() { - return new FilterPattern(CONSTANT); - } - - void add(Expr exp) { - args.add(exp); - } - - public int arity() { - return args.size(); - } - - void setRec() { - rec = true; - } - - boolean isRec() { - return rec; - } - - void setMatchConstant() { - matchConstant = false; - } - - boolean isMatchConstant() { - return matchConstant; - } - - public Expr getExp(int i) { - return args.get(i); - } - - public List getExpList() { - return args; - } - - public Filter getFilter() { - return null; - } - - public int getIndex() { - return 0; - } - - public String getLabel() { - return label; - } - - void setLabel(String l) { - label = l; - } - - @Override - public DatatypeValue getValue() { - return null; - } - - public boolean isAggregate() { - return false; - } - - public boolean isBound() { - return false; - } - - public int oper() { - return oper; - } - - public void setIndex(int index) { - // FilterPattern indices are immutable - } - - public int type() { - return type; - } - - public String toString() { - return "pat(" + type + ", " + oper + ")"; - } - - @Override - public Exp getPattern() { - return null; - } - - @Override - public boolean isDistinct() { - return false; - } - - @Override - public String getModality() { - return null; - } - - @Override - public boolean isVariable() { - return false; - } - - @Override - public Expr getArg() { - return null; - } - - @Override - public void setArg(Expr exp) { - // FilterPattern arguments are immutable - } - - @Override - public boolean isRecAggregate() { - return false; - } - - @Override - public boolean isExist() { - return false; - } - - @Override - public boolean isRecExist() { - return false; - } - - @Override - public boolean isFuncall() { - return false; - } - - @Override - public void setOper(int n) { - // FilterPattern operator is immutable - } - - @Override - public void setExp(int i, Expr e) { - // FilterPattern sub-expressions are immutable - } - - @Override - public Expr getDefine() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public void setDefine(Expr exp) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public int subtype() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public Expr getFunction() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public Expr getBody() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public Expr getVariable() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public Expr getDefinition() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public boolean isSystem() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public boolean isTrace() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public boolean isDebug() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public boolean isPublic() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public DatatypeValue getDatatypeValue() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public void setPublic(boolean b) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public void setSubtype(int n) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public boolean match(int oper) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public boolean isConstant() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public boolean hasMetadata(String type) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public boolean isDynamic() { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - - @Override - public DatatypeValue evalWE(Evaluator eval, BindingContext b, Environment env, Producer p) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } + int type; + int oper; + String label; + // recursive pattern, ako * + boolean rec = false; + boolean matchConstant = true; + List args; + + /** Copies optimizer metadata into the execution graph. */ + @Override + public FilterPattern copyPlan(PlanCopy copies) { + FilterPattern result = new FilterPattern(type); + copies.remember(this, result); + result.oper = oper; + result.label = label; + result.rec = rec; + result.matchConstant = matchConstant; + result.args = copies.copy(args); + return result; + } + + FilterPattern(int t) { + type = t; + oper = JOKER; + args = new ArrayList<>(); + } + + FilterPattern(int t, int o) { + this(t); + oper = o; + } + + FilterPattern(int t, int o, Expr e1) { + this(t, o); + add(e1); + } + + FilterPattern(int t, int o, Expr e1, Expr e2) { + this(t, o, e1); + add(e2); + } + + public FilterPattern(int t, int o, int e1, int e2){ + this(t, o); + add(new FilterPattern(e1)); + add(new FilterPattern(e2)); + } + + static FilterPattern variable(String label) { + FilterPattern p = new FilterPattern(VARIABLE); + p.setLabel(label); + return p; + } + + static FilterPattern constant() { + return new FilterPattern(CONSTANT); + } + + void add(Expr exp) { + args.add(exp); + } + + public int arity() { + return args.size(); + } + + void setRec() { + rec = true; + } + + boolean isRec() { + return rec; + } + + void setMatchConstant() { + matchConstant = false; + } + + boolean isMatchConstant() { + return matchConstant; + } + + public Expr getExp(int i) { + return args.get(i); + } + + public List getExpList() { + return args; + } + + public Filter getFilter() { + return null; + } + + public int getIndex() { + return 0; + } + + public String getLabel() { + return label; + } + + void setLabel(String l) { + label = l; + } + + @Override + public DatatypeValue getValue() { + return null; + } + + public boolean isAggregate() { + return false; + } + + public int oper() { + return oper; + } + + public void setIndex(int index) { + // FilterPattern indices are immutable + } + + public int type() { + return type; + } + + public String toString() { + return "pat(" + type + ", " + oper + ")"; + } + + @Override + public Exp getPattern() { + return null; + } + + @Override + public boolean isVariable() { + return false; + } + + @Override + public boolean isRecAggregate() { + return false; + } + + @Override + public boolean isRecExist() { + return false; + } + + @Override + public int subtype() { + throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + } + + @Override + public boolean match(int oper) { + throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + } + + @Override + public boolean isConstant() { + throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + } + + @Override + public DatatypeValue evalWE(Evaluator eval, BindingContext b, Environment env, Producer p) { + throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/MatchBind.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/MatchBind.java index 49dcd401e..fd1ba114c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/MatchBind.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/MatchBind.java @@ -11,61 +11,61 @@ /** - * Manage bindings of Matcher + * Manage bindings of ExpressionPatternMatcher * Can bind and unbind * * @author Olivier Corby, Edelweiss, INRIA 2010 * */ public class MatchBind { - private static final Logger logger = LoggerFactory.getLogger(MatchBind.class); + private static final Logger logger = LoggerFactory.getLogger(MatchBind.class); - Hashtable table; - List stack; + Hashtable table; + List stack; - MatchBind (){ - table = new Hashtable<>(); - stack = new ArrayList<>(); - } + MatchBind (){ + table = new Hashtable<>(); + stack = new ArrayList<>(); + } - static MatchBind create(){ - return new MatchBind(); - } + static MatchBind create(){ + return new MatchBind(); + } - boolean hasValue(Expr exp){ - return table.containsKey(exp); - } + boolean hasValue(Expr exp){ + return table.containsKey(exp); + } - Expr getValue(Expr exp){ - return table.get(exp); - } + Expr getValue(Expr exp){ + return table.get(exp); + } - void setValue(Expr qe, Expr te){ - if (qe instanceof FilterPattern){ - table.put(qe, te); - stack.add(qe); - } - } + void setValue(Expr qe, Expr te){ + if (qe instanceof FilterPattern){ + table.put(qe, te); + stack.add(qe); + } + } - void reset(Expr exp){ - table.remove(exp); - stack.remove(exp); - } + void reset(Expr exp){ + table.remove(exp); + stack.remove(exp); + } - int size(){ - return stack.size(); - } + int size(){ + return stack.size(); + } - /** - * Remove last bindings until stack.size() = size - */ - void clean(int size){ - while (stack.size()>size){ - reset(stack.getLast()); - } - if (table.size() != stack.size()){ - logger.error("Match Bind: Stack and Table have different sizes"); - } - } + /** + * Remove last bindings until stack.size() = size + */ + void clean(int size){ + while (stack.size()>size){ + reset(stack.getLast()); + } + if (table.size() != stack.size()){ + logger.error("Match Bind: Stack and Table have different sizes"); + } + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/Matcher.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/Matcher.java deleted file mode 100644 index ccf2f8094..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/filter/Matcher.java +++ /dev/null @@ -1,230 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.filter; - -import fr.inria.corese.core.next.query.impl.engine.model.Expr; -import fr.inria.corese.core.next.query.impl.engine.model.ExprType; - - -/** - * Filter Exp Matcher - * - * @author Olivier Corby, Edelweiss, INRIA 2010 - * - */ -public class Matcher implements ExprType { - boolean rec = false; - boolean matchConstant = true; - - - public boolean match(FilterPattern qe, Expr te){ - MatchBind bind = MatchBind.create(); - return match(qe, te, bind); - } - - boolean match(FilterPattern qe, Expr te, MatchBind bind){ - rec = qe.isRec(); - matchConstant = qe.isMatchConstant(); - return process(qe, te, bind); - } - - - - - /** - * Expr matcher - * Special case: - * OR(EXP) with rec=true - * means match OR(EXP) recursively with OR(EXP1, OR(EXP2, EXP3)) - * EXP is matched with all EXPi - * Pattern variable store target variable - * Afterwards, Pattern variable must match same target variables in all EXPi - * - */ - boolean process(Expr qe, Expr te, MatchBind bind) { - if (qe.type() == ALTER) { - return matchAlternative(qe, te, bind); - } - if (!matchType(qe, te)) { - return false; - } - if ((qe.type() != CONSTANT || matchConstant) && bind.hasValue(qe)) { - return process(bind.getValue(qe), te, bind); - } - return switch (qe.type()) { - case VARIABLE, CONSTANT -> matchVarConst(qe, te, bind); - case FUNCTION, JOKER -> matchBoolFunAny(qe, te, bind); - case BOOLEAN -> matchBoolean(qe, te, bind); - case TERM -> matchTerm(qe, te, bind); - default -> false; - }; - } - - private boolean matchAlternative(Expr qe, Expr te, MatchBind bind) { - for (Expr alternative : qe.getExpList()) { - int size = bind.size(); - if (process(alternative, te, bind)) { - return true; - } - bind.clean(size); - } - return false; - } - - private boolean matchBoolean(Expr qe, Expr te, MatchBind bind) { - if (qe.arity() == 2 && te.arity() == 2) { - return matchTerm(qe, te, bind); - } - return qe.arity() <= te.arity() && matchBoolFunAny(qe, te, bind); - } - - boolean matchVarConst(Expr qe, Expr te, MatchBind bind) { - if (qe.type() == VARIABLE) { - if (qe.getLabel() != null) { - return qe.getLabel().equals(te.getLabel()); - } - bind.setValue(qe, te); - } else if (qe.type() == CONSTANT && matchConstant) { - if (!isBindable(qe)) { - return qe.equals(te); - } - bind.setValue(qe, te); - } - return true; - } - - - /** - * BOOLEAN NOT - * BOOLEAN with rec = true - * OR(EXP) matches recursively OR(OR(EXP1, EXP2), EXP3) - * FUNCTION ANY - */ - boolean matchBoolFunAny(Expr qe, Expr te, MatchBind bind) { - if (qe.arity() == 0) { - // qe e.g. Pattern(ANY, ANY) with no args - // first time: bind the pattern - bind.setValue(qe, te); - return true; - } - if (te.arity() == 0) { - // qe has arity, te has not: fail - return false; - } - - int size = bind.size(); - int i = 0; - Expr qarg; - - for (Expr targ : te.getExpList()) { - if (i < qe.arity()) { - // use pattern ith arg - qarg = qe.getExp(i++); - } else if (rec) { - // reuse pattern 0th arg - qarg = qe.getExp(0); - } else { - // query has less arguments than target - // and query args match target args - // should reset bindings here - bind.clean(size); - return false; - } - - if (!process(qarg, targ, bind) && (!rec || !process(qe, targ, bind))) { - bind.clean(size); - return false; - } - } - - if (!bind.hasValue(qe)) { - // because when rec = true, qe may have already been bound to an arg of te - bind.setValue(qe, te); - } - return true; - } - - - boolean matchTerm(Expr qe, Expr te, MatchBind bind) { - Expr fst = qe.getExp(0); - Expr snd = qe.getExp(1); - - if (isGL(qe.oper()) && qe.oper() != te.oper()) { - - if (inverse(qe.oper(), te.oper())) { - // qe(x < y) VS te(y > x) - // try switch qe args - fst = qe.getExp(1); - snd = qe.getExp(0); - } - else return false; - } - - int size = bind.size(); - boolean b = - process(fst, te.getExp(0), bind) && - process(snd, te.getExp(1), bind); - - if (! b){ - bind.clean(size); - - if (symmetric(qe)){ - // try switch arguments of qe - // switch ?x = ?y to ?y = ?x - b = - process(snd, te.getExp(0), bind) && - process(fst, te.getExp(1), bind); - } - } - - if (b) bind.setValue(qe, te); - return b; - } - - /** - * Is it a Pattern constant - */ - boolean isBindable(Expr exp){ - return exp instanceof FilterPattern; - } - - boolean symmetric(Expr qe){ - if (qe.type() == BOOLEAN && qe.oper() == JOKER) return true; - int t = qe.oper(); - return t == EQ || t == NE || t == PLUS || t == MULT || t == AND || t == OR; - } - - boolean inverse(int t1, int t2){ - return - (t1 == LT && t2 == GT) || - (t1 == GT && t2 == LT) || - (t1 == GE && t2 == LE) || - (t1 == LE && t2 == GE) ; - } - - boolean isGL(int tt){ - return tt == GL || tt == LT || tt == LE || tt == GT || tt == GE; - } - - - /** - * accept comparison operator with any comparison because term matcher - * will permute arguments as needed - */ - boolean match(int qt, int tt){ - if (qt == JOKER) return true; - if (isGL(qt)) return isGL(tt); - if (qt == EQNE) return tt == EQ || tt == NE; - return qt == tt; - } - - boolean matchType(Expr qe, Expr te){ - if (qe.oper() == EXIST || te.oper() == EXIST) return false; - if (qe.type() == JOKER) { - return true; - } - return switch (qe.type()) { - case VARIABLE, CONSTANT -> match(qe.type(), te.type()); - default -> match(qe.type(), te.type()) && match(qe.oper(), te.oper()); - }; - } - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/EdgeInv.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/EdgeInv.java index 1a2b0c8cc..9847f610d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/EdgeInv.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/EdgeInv.java @@ -105,4 +105,4 @@ public Iterable getLoop() { public PointerType pointerType() { return TRIPLE; } -} \ No newline at end of file +} 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 58e565c57..c65262042 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 @@ -18,49 +18,47 @@ public interface ExpType { enum Type { - EMPTY("EMPTY"), - AND("AND"), - UNION("UNION"), - GRAPH("GRAPH"), - OPTION("OPTION"), - EDGE("EDGE"), - FILTER("FILTER"), - NODE("NODE"), - BGP("BGP"), - WATCH("WATCH"), - CONTINUE("CONTINUE"), - BACKJUMP("BACKJUMP"), - EXTERN("EXTERN"), - QUERY("QUERY"), - FORALL("FORALL"), - EXIST("EXIST"), - GRAPHNODE("GRAPHNODE"), - OPTIONAL("OPTIONAL"), - SCAN("SCAN"), - IF("IF"), - PATH("PATH"), - XPATH("XPATH"), - ACCEPT("ACCEPT"), - BIND("BIND"), - EVAL("EVAL"), - SCOPE("SCOPE"), - TEST("TEST"), - NEXT("NEXT"), - MINUS("MINUS"), - POP("POP"), - SERVICE("POP"), - RESTORE("RESTORE"), - JOIN("JOIN"), - VALUES("VALUES"), - OPT_BIND("OPT_BIND"); - private final String title; - Type(String title) { - this.title = title; - } + EMPTY("EMPTY"), + AND("AND"), + UNION("UNION"), + GRAPH("GRAPH"), + OPTION("OPTION"), + EDGE("EDGE"), + 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; + } - public String getTitle() { - return title; - } - } + public String getTitle() { + return title; + } + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/Expr.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/Expr.java index 01ba57e95..973828805 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/Expr.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/Expr.java @@ -1,5 +1,7 @@ package fr.inria.corese.core.next.query.impl.engine.model; +import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; + import fr.inria.corese.core.next.query.impl.engine.spi.Environment; import fr.inria.corese.core.next.query.impl.engine.spi.Evaluator; import fr.inria.corese.core.next.query.impl.engine.spi.Producer; @@ -8,7 +10,9 @@ import java.util.List; /** - * Expr api refer to sparql.triple.parser.Expression + * Compiled expression contract used by runtime evaluation and filter planning. + * Constants use the native datatype model; nested EXISTS patterns expose compiled + * operators rather than parser or syntax-tree objects. * * @author Olivier Corby, Edelweiss, INRIA 2010 */ @@ -16,35 +20,15 @@ public interface Expr { Filter getFilter(); - // Exp as Object for modularity - Object getPattern(); - - boolean isSystem(); - - boolean isPublic(); - - void setPublic(boolean b); - - boolean isDynamic(); - - boolean isTrace(); - - boolean isDebug(); + /** Returns a compiled EXISTS pattern, or null when this expression has none. */ + Exp getPattern(); String getLabel(); - String getModality(); - List getExpList(); Expr getExp(int i); - void setExp(int i, Expr e); - - Expr getArg(); - - void setArg(Expr exp); - /** * Returns the constant value of this expression, if any. * @@ -52,63 +36,30 @@ public interface Expr { */ DatatypeValue getValue(); - /** - * Returns the datatype value of this expression. - * - * @return the datatype value - */ - DatatypeValue getDatatypeValue(); - int type(); int subtype(); - void setSubtype(int n); - int oper(); boolean match(int oper); - void setOper(int n); - boolean isAggregate(); boolean isRecAggregate(); - boolean isExist(); - boolean isRecExist(); boolean isVariable(); boolean isConstant(); - boolean isFuncall(); - - boolean isBound(); - - boolean isDistinct(); - int arity(); int getIndex(); void setIndex(int index); - Expr getDefine(); - - void setDefine(Expr exp); - - Expr getFunction(); - - Expr getBody(); - - Expr getVariable(); - - Expr getDefinition(); - - boolean hasMetadata(String name); - /** * Evaluates this expression with the given context. * 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 5454e0613..c6192f8e9 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 @@ -43,7 +43,6 @@ public interface ExprType { int DATATYPE = 30; int SELF = 32; int DEBUG = 33; - int EXTERN = 35; int KGRAM = 36; int SQL = 37; int XPATH = 38; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/NodeImpl.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/NodeImpl.java index 2309b3f85..10faefed2 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/NodeImpl.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/NodeImpl.java @@ -1,5 +1,8 @@ package fr.inria.corese.core.next.query.impl.engine.model; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopy; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopyable; + import fr.inria.corese.core.next.data.Values; import fr.inria.corese.core.next.data.api.model.DatatypeValue; import fr.inria.corese.core.next.query.impl.engine.path.Path; @@ -7,7 +10,7 @@ import java.util.Objects; /** Native KGRAM node backed exclusively by Corese-next value contracts. */ -public final class NodeImpl implements Node { +public final class NodeImpl implements Node, PlanCopyable { private DatatypeValue value; private final String variableName; @@ -26,6 +29,17 @@ private NodeImpl(DatatypeValue value, String variableName, boolean blankVariable this.blankVariable = blankVariable; } + /** Copies the indexed node without sharing mutable payload state. */ + @Override + public NodeImpl copyPlan(PlanCopy copies) { + NodeImpl target = new NodeImpl(value, variableName, blankVariable); + copies.remember(this, target); + target.index = index; + target.key = key; + target.payload = copies.copy(payload); + return target; + } + /** Creates a constant node carrying a Corese-next RDF value. */ public static NodeImpl forValue(DatatypeValue value) { return new NodeImpl(Objects.requireNonNull(value, "value"), null); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/Regex.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/Regex.java deleted file mode 100644 index cb505c6d0..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/model/Regex.java +++ /dev/null @@ -1,84 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.model; - -import fr.inria.corese.core.next.data.api.model.DatatypeValue; - -/** - * Interface of Property Path Regex - * - * @author Olivier Corby, Edelweiss, INRIA 2010 - */ -public interface Regex { - - int UNDEF = -1; - int LABEL = 0; - int NOT = 1; - int SEQ = 2; - int STAR = 3; - int PLUS = 4; - int OPTION = 5; - int COUNT = 6; - int ALT = 7; - int PARA = 8; - int TEST = 9; - int CHECK = 10; - - String getName(); - - String getLongName(); - - DatatypeValue getDatatypeValue(); - - int retype(); - - int getArity(); - - boolean isConstant(); - - boolean isAlt(); - - boolean isSeq(); - - boolean isOpt(); - - boolean isNot(); - - boolean isDistinct(); - - boolean isShort(); - - // @deprecated - boolean isInverse(); - - // @deprecated - void setInverse(boolean b); - - // SPARQL 1.1 reverse ^ - boolean isReverse(); - - void setReverse(boolean b); - - boolean isStar(); - - boolean isPlus(); - - boolean isCounter(); - - int getMin(); - - int getMax(); - - int getWeight(); - - Regex getArg(int n); - - Regex reverse(); - - Regex transform(); - - Regex translate(); - - int regLength(); - - Expr getExpr(); - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/package-info.java index c96eb4718..0b428d9f7 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/package-info.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/package-info.java @@ -15,14 +15,14 @@ * transit through the {@link fr.inria.corese.core.next.query.impl.engine.spi.Producer} * and Storage SPI abstractions. *
  • No Bloat in Core Engine Classes: Feature additions must avoid appending - * large chunks of code directly into central classes such as {@code Eval} (~72 KB) - * or {@code Exp} (~42 KB). Specific semantics should be isolated into dedicated helper + * large chunks of code directly into central classes such as {@code Eval} + * or {@code Exp}. Specific semantics should be isolated into dedicated helper * components or evaluators when this improves separation of responsibilities, rather than * being appended directly to the core engine classes.
  • *
  • Thread Isolation: Execution contexts and solution mappings must remain * isolated without global mutable state.
  • *
  • W3C Zero-Regression: All engine developments, feature additions, and - * refactorings must strictly preserve the green non-regression baseline established + * refactorings must strictly preserve the non-regression baseline established * in {@code corese-w3c}.
  • * */ diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/GraphPath.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/GraphPath.java deleted file mode 100644 index 97ca99f8b..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/GraphPath.java +++ /dev/null @@ -1,37 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.path; - - -import fr.inria.corese.core.next.query.impl.engine.spi.Environment; - -/** - * Draft to compute path in the graph - * - * @author Olivier Corby, Edelweiss, INRIA 2010 - */ -public class GraphPath extends Thread { - - - private final Environment mem; - private final PathFinder finder; - - - - /** - * ?x %path ?y - * case: - * ?x or ?y is bound/unbound - * filter on ?x ?y - * Relation type on %path : ?x c:related::%path ?y - * - */ - - public GraphPath(PathFinder pc, Environment mem){ - this.finder = pc; - this.mem = mem; - } - - @Override - public void run() { - finder.process(finder.get(mem, finder.getIndex()), mem); - } -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/Path.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/Path.java index 37503f528..244c26d0b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/Path.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/Path.java @@ -20,7 +20,7 @@ * * @author Olivier Corby, Edelweiss, INRIA 2010 */ -public class Path extends ProducerDefault implements Pointerable { +public final class Path extends ProducerDefault implements Pointerable { boolean isShort = false; boolean isReverse = false; @@ -178,4 +178,4 @@ public Iterable getLoop() { public Edge getValue(String variable, int n) { return edges.get(n); } -} \ No newline at end of file +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PathFinder.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PathFinder.java deleted file mode 100644 index d3c70b482..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PathFinder.java +++ /dev/null @@ -1,1140 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.path; - -import fr.inria.corese.core.next.query.impl.engine.eval.Eval; -import fr.inria.corese.core.next.query.impl.engine.pattern.Query; -import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; -import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; -import fr.inria.corese.core.next.query.impl.engine.solution.Memory; - -import fr.inria.corese.core.next.query.impl.engine.model.Edge; -import fr.inria.corese.core.next.query.impl.engine.model.Filter; -import fr.inria.corese.core.next.query.impl.engine.model.Node; -import fr.inria.corese.core.next.query.impl.engine.model.Regex; -import fr.inria.corese.core.next.query.impl.engine.spi.Environment; -import fr.inria.corese.core.next.query.impl.engine.spi.Evaluator; -import fr.inria.corese.core.next.query.impl.engine.spi.Matcher; -import fr.inria.corese.core.next.query.impl.engine.spi.Producer; -import fr.inria.corese.core.next.query.api.exception.QueryException; -import fr.inria.corese.core.next.query.impl.engine.event.ResultListener; -import fr.inria.corese.core.next.query.impl.engine.model.EdgeInv; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; - -/** - * ******************************************************** - * ?x rdf:resf * /rdf:first - * write paths in a synchronized buffer - * ?x ^(p/q) ?y ::= ?y p/q ?x -> ?x inv(q)/inv(p) ?y - * ?x p/q uri path is computed backward from uri to ?x with index=1 other=0 - * isReverse=true sequence is eval as q/p, all other exp are the same - * ?x ^(p/q) uri ::= uri p/q ?x -> ?x inv(q)/inv(p) uri - * PP Extensions - * Path Variable: ?x exp :: $path ?y - * Path Length: pathLength($path) - * Path Enumeration: ?x exp :: $path ?y graph $path {?a ?p ?b} - * Shortest path: ?x distinct short exp ?y ; ?x short exp ?y -- Note: complete - * short to get only shortest - * Path weight: ?x (rdf:first@2 / rdf:rest@1* / ^rdf:first@2) * ?y - * Constaint: ?x exp - * @ {?this a foaf:Person} ?y ?x exp - * @ [a foaf:Person] ?y - * Parallel Path: ?x (foaf:knows || ^rdfs:seeAlso) + ?y - * Check Loop: pf.setCheckLoop(true) => exp+ exp[n,m] without loop - * exec.setPathLoop(false) pragma {kg:path kg:loop false} - * - * @author Olivier Corby, Edelweiss, INRIA 2010 - * @ thanx Corentin Follenfant for the idea of property weight into the regex - * - ******************************************************** - */ -public class PathFinder { - - private static final Logger logger = LoggerFactory.getLogger(PathFinder.class); - - // synchronized buffer between this and projection - private PathMappingBuffer mbuffer; - private Environment memory; - private ResultListener listener; - private Eval kgram; - private final Producer producer; - private final Matcher matcher; - private final Evaluator evaluator; - private final Query query; - private Mappings lMap; - private final HashMap store; - private Filter filter; - private Memory mem; - private Edge edge; - private Node gNode; - private Node targetNode; - private Node regexNode; - private Node varNode; - private List from; - private Node[] qNodes; - - // index of node in edge that is the start of the path - private int index = 0; - // the inverse of the index (i.e. the other arg) - private int other; - private boolean isStop = false; - private boolean hasListener = false; - private boolean isDistinct = false; - - private boolean isReverse; - private boolean isShort; - private boolean isOne; - - // if true: return list of path instead of thread buffer: 50% faster but enumerate all path - private boolean isList = false; - private boolean checkLoop = false; - private boolean isCountPath = false; - private boolean isCache = false; - private boolean isStorePath = true; - - private static final int MAX_LENGTH = Integer.MAX_VALUE; - private int min = 0; - private int max = MAX_LENGTH; - - private Regex regexp; - - /** - * @param isStorePath the isStorePath to set - */ - public void setStorePath(boolean isStorePath) { - this.isStorePath = isStorePath; - } - - /** - * @return the isCache - */ - public boolean isCache() { - return isCache; - } - - /** - * @param isCache the isCache to set - */ - public void setCache(boolean isCache) { - this.isCache = isCache; - } - - public PathFinder(Producer p, Matcher match, Evaluator eval, Query q) { - query = q; - producer = p; - matcher = match; - evaluator = eval; - lMap = new Mappings(); - store = new HashMap<>(); - } - - public static PathFinder create(Eval eval, Producer p, Query q) { - PathFinder pf = new PathFinder(p, eval.getMatcher(), eval.getEvaluator(), q); - pf.setEval(eval); - return pf; - } - - void setEval(Eval ev) { - kgram = ev; - } - - public void set(ResultListener rl) { - listener = rl; - hasListener = rl != null; - } - - public void setCheckLoop(boolean b) { - checkLoop = b; - } - - public void setCountPath(boolean b) { - isCountPath = b; - if (b) { - isDistinct = false; - } - } - - public void setList(boolean b) { - isList = b; - } - - /** - * Start/init computation of a new list of path - */ - public void start(Edge edge, Node node, Memory env, Filter f) { - regexNode = node; - List lVar = null; - if (f != null) { - lVar = f.getVariables(); - } - lMap = new Mappings(); - this.edge = edge; - int n = index(edge, env, lVar); - start(n); - index = n; - targetNode = env.getNode(edge.getNode(other)); - varNode = edge.getEdgeVariable(); - if (f != null && match(edge, lVar, index)) { - filter = f; - init(env); - } - if (mem == null && node != null) { - init(env); - } - } - - void init(Memory env) { - mem = new Memory(matcher, evaluator); - mem.init(env.getQuery()); - mem.init(env); - evaluator.init(mem); - mem.share(mem.getBind(), env.getBind()); - mem.setEval(kgram); - } - - boolean match(Edge edge, List lVar, int index) { - return lVar.size() == 1 - && edge.getNode(index).isVariable() - && edge.getNode(index).getLabel().equals(lVar.getFirst()); - } - - /** - * Compute the index of node of edge which will be start of the path If one - * of the nodes is bound, this is it If one of the nodes is a constant, this - * is it If there is a filter on a node, this is it Otherwise it is node at - * index 0 - */ - int index(Edge edge, Environment mem, List lVar) { - // which arg is bound if any ? - for (int i = 0; i < 2; i++) { - if (mem.isBound(edge.getNode(i))) { - return i; - } - } - for (int i = 0; i < 2; i++) { - if (edge.getNode(i).isConstant()) { - return i; - } - } - if (lVar != null) { - for (int i = 0; i < 2; i++) { - if (match(edge, lVar, i)) { - return i; - } - } - } - return 0; - } - - /** - * Enumerate all path, return a list of path 50% faster that thread but - * enumerate *all* path - */ - public Iterable candidate2(Node gNode, List from, Environment mem) { - this.gNode = gNode; - this.from = from; - this.memory = mem; - Node cstart = get(memory, index); - - Mappings map = getMappings(cstart); - if (map != null) { - return map; - } - - lMap = new Mappings(); - process(cstart, memory); - putMappings(cstart, lMap); - return lMap; - } - - /** - * Retrieve solution in cache Note: manage two tables for two possible index - */ - Mappings getMappings(Node cstart) { - if (isCache() && cstart != null) { - return store.get(cstart.getIndex()); - } - return null; - } - - void putMappings(Node start, Mappings map) { - if (isCache() && start != null) { - store.put(start.getIndex(), map); - } - } - - /** - * Enumerate path in a parallel thread, return a synchronised buffer Useful - * if backjump or have a limit in sparql query - */ - public Iterable candidate(Node gNode, List from, Environment env) { - isStop = false; - if (mem != null) { - mem.setGraphNode(gNode); - } - if (isList) { - return candidate2(gNode, from, env); - } - this.gNode = gNode; - this.from = from; - this.memory = env; - mstart(env); - // return path enumeration (read the synchronized buffer) - return mbuffer; - } - - int getIndex() { - return index; - } - - void mstart(Environment mem) { - // buffer store path enumeration - mbuffer = new PathMappingBuffer(); - // path enumeration in a thread - GraphPath graphPath = new GraphPath(this, mem); - // launch path computing (one by one) eg launch process() below - graphPath.start(); - } - - public void stop() { - isStop = true; - } - - /** - * init at creation time, no need to change. pmax comes from pathLength() <= - * pmax - */ - public void init(Regex exp, int pmin, int pmax) { - regexp = exp; - - isReverse = false; - isShort = false; - isOne = false; - other = 1; - - if (exp.isShort()) { - isShort = true; - if (exp.isDistinct()) { - isOne = true; - } - } - - // set min and max path length - // filter pathLength($path) > val - // the length of the minimal path that matches regex: - int length = regexp.regLength(); - min = Math.max(min, length); - max = Math.max(max, length); - - // user default values - if (pmin != -1) { - min = Math.max(min, pmin); - } - // either IntegerMax or pathLength($p) <= max - if (pmax != -1) { - max = pmax; - } - } - - public Edge getEdge() { - return edge; - } - - /** - * start at run time depends on index : which arg is bound or where we start - */ - void start(int n) { - index = n; - if (index == 1) { - other = 0; - isReverse = true; - } else { - other = 1; - isReverse = false; - } - } - - Node get(Environment memory, int i) { - if (edge == null) { - return null; - } - Node qc = edge.getNode(i); - return memory.getNode(qc); - } - - void process(Node cstart, Environment memory) { - // Is the source of edge bound ? - // In which case all path relations come from same source - Node csource = null; - if (gNode != null) { - csource = gNode.isConstant() ? gNode : memory.getNode(gNode); - } - - // the start concept for path - Path path = new Path(isReverse); - path.setMax(max); - path.setIsShort(isShort); - - if (isShort && cstart != null) { - // if null, will be done later - producer.initPath(edge, 0); - } - - Regex regexp1 = regexp.transform(); - eval(regexp1, path, cstart, csource); - - // in order to stop enumeration, return null - if (!isList) { - mbuffer.put(null, false); - } - } - - /** - * Path result as a Mapping start and last nodes the path variable whose - * index is used (e.g. pathLength) to retrieve the list of edges in the - * memory the list of edges - */ - private Mapping result(Path path, Node gNode, Node src, Node start, boolean isReverse) { - Edge ee = edge; - int length = 3; - int ip = 2; - int is = 3; - - if (!isStorePath) { - length = 2; - is = 2; - } - if (src != null) { - length += 1; - } - - Node n1; - Node n2; - if (path.size() == 0) { - n1 = start; - n2 = start; - } else { - n1 = path.firstNode(); - n2 = path.lastNode(); - } - - if (!check(n1, n2)) { - return null; - } - - if (qNodes == null) { - // computed once and then shared by all Mapping - qNodes = new Node[length]; - qNodes[0] = ee.getNode(0); - qNodes[1] = ee.getNode(1); - if (src != null) { - qNodes[is] = gNode; - } - if (isStorePath) { - qNodes[ip] = ee.getEdgeVariable(); - } - } - - Node[] tNodes = new Node[length]; - tNodes[0] = n1; - tNodes[1] = n2; - - if (src != null) { - tNodes[is] = src; - } - - Mapping map = Mapping.create(qNodes, tNodes); - if (isStorePath) { - Path edges = path.copy(producer); - if (isReverse) { - edges.reverse(); - } - tNodes[ip] = getPathNode(edges); - } - - return map; - } - - /** - * Generate a unique Blank Node wrt query that represents the path Use a - * predefined filter pathNode() - */ - Node getPathNode(Path p) { - Filter f = query.getGlobalFilter(Query.PATHNODE); - Node node = producer.getNode( - f.getExp().evalWE(evaluator, memory.getBind(), memory, producer)); - if (node == null) { - throw new IllegalStateException("The path-node expression returned no RDF value"); - } - node.setObject(p); - return node; - } - - /** - * Check if target node match its query node and its binding - */ - private boolean check(Node n0, Node n1) { - if (index == 0) { - if (!matcher.match(edge.getNode(1), n1, memory)) { - return false; - } - return targetNode == null || targetNode.match(n1); - } else { - if (!matcher.match(edge.getNode(0), n0, memory)) { - return false; - } - return targetNode == null || targetNode.match(n0); - } - } - - /** - * cstart is bound when edge node is a bound variable - * cstart is not bound when edge node is a constant, this case - * is processed by Producer - */ - private Iterable getNodeIterator(Node cstart) { - if (cstart == null) { - return getNodeIterator(edge, from, null); - } else { - // start is bound - ArrayList list = new ArrayList<>(); - list.add(cstart); - return list; - } - } - - public Iterable getNodeIterator(final Edge edge, List from, List regex) { - Iterable iter = producer.getNodes(gNode, from, edge, memory, regex, index); - - if (filter == null) { - return iter; - } - - final Iterator it = iter.iterator(); - - return () -> new Iterator<>() { - @Override - public boolean hasNext() { - return it.hasNext(); - } - - @Override - public Node next() { - while (hasNext()) { - Node entity = it.next(); - if (entity == null) { - return null; - } - Node node = entity.getNode(); - if (test(node)) { - return entity; - } - } - return null; - } - }; - } - - /** - * exp - * @ {?this rdf:type c:Person} - */ - boolean test(Filter filter, Path path, Node qNode, Node node) { - mem.push(qNode, node); - if (varNode != null) { - Node pathNode = getPathNode(path); - mem.push(varNode, pathNode); - } - boolean test; - try { - test = filter.getExp().test(evaluator, mem.getBind(), mem, producer); - } catch (QueryException ex) { - test = false; - } - mem.pop(qNode); - if (varNode != null) { - mem.pop(varNode); - } - return test; - } - - boolean test(Node node) { - Node qNode = edge.getNode(index); - mem.push(qNode, node); - boolean test; - try { - test = filter.getExp().test(evaluator, mem.getBind(), mem, producer); - } catch (QueryException ex) { - test = false; - } - mem.pop(qNode); - return test; - } - - /* - * ******************************************************************************* - * - * New version interprets regex directly with a stack - * - ******************************************************************************* - */ - /** - * rewrite ! (^ p) as ^ (! p) rewrite ^(p/q) as ^q/^p - */ - void eval(Regex exp, Path path, Node start, Node src) { - Record stack = new Record(Visit.create(isReverse, isCountPath)); - stack.push(exp); - try { - eval(stack, path, start, src); - } catch (StackOverflowError e) { - logger.error("** Property Path Size: {}", path.size()); - logger.error("** Property Path Error: \n{}", String.valueOf(e)); - } - } - - /** - * top of stack is current exp to eval rest of stack is in sequence path may - * be walked left to right if start is bound or right to left if end is - * bound in the later case, index = 1 - */ - void eval(Record stack, Path path, Node start, Node src) { - if (isStop) { - return; - } - - if (stack.isEmpty()) { - if (stack.getTarget() != null) { - // this is a parallel path check, path is finished: stop it - if (start.match(stack.getTarget())) { - // it is successful - stack.setSuccess(); - } - return; - } - - result(stack, path, start, src); - return; - } - - Regex exp = stack.pop(); - - switch (exp.retype()) { - case Regex.TEST: - evalTest(exp, stack, path, start, src); - break; - - case Regex.LABEL, Regex.NOT: - evalLabel(exp, stack, path, start, src); - break; - - case Regex.SEQ: - evalSeq(exp, stack, path, start, src); - break; - - case Regex.PARA: - evalPara(exp, stack, path, start, src); - break; - - case Regex.CHECK: - evalCheck(exp, stack, path, start, src); - break; - - case Regex.PLUS: - // exp+ - if (start == null && stack.getVisit().knows(exp)) { - stack.push(exp); - return; - } - plus(exp, stack, path, start, src); - break; - - case Regex.COUNT: - // regex count: exp[1, n] - count(exp, stack, path, start, src); - break; - - case Regex.STAR: - // exp* - if (start == null && stack.getVisit().knows(exp)) { - stack.push(exp); - return; - } - star(exp, stack, path, start, src); - break; - - case Regex.ALT: - evalAlt(exp, stack, path, start, src); - break; - - case Regex.OPTION: - option(exp, stack, path, start, src); - break; - - default: - break; - } - } - - private void evalTest(Regex exp, Record stack, Path path, Node start, Node src) { - boolean b = true; - if (start != null) { - b = test(exp.getExpr().getFilter(), path, regexNode, start); - } - if (b) { - eval(stack, path, start, src); - } - stack.push(exp); - } - - private void evalSeq(Regex exp, Record stack, Path path, Node start, Node src) { - int fst = 0; - int rst = 1; - if (isReverse) { - fst = 1; - rst = 0; - } - - stack.push(exp.getArg(rst)); - stack.push(exp.getArg(fst)); - - eval(stack, path, start, src); - - stack.pop(); - stack.pop(); - stack.push(exp); - } - - private void evalPara(Regex exp, Record stack, Path path, Node start, Node src) { - if (start != null) { - stack.pushStart(start); - } - stack.push(exp.getArg(2)); - stack.push(exp.getArg(0)); - eval(stack, path, start, src); - stack.pop(); - stack.pop(); - - if (start != null) { - stack.popStart(); - } - stack.push(exp); - } - - private void evalCheck(Regex exp, Record stack, Path path, Node start, Node src) { - Regex test = exp.getArg(0); - - if (test.retype() == Regex.PARA) { - Record st = new Record(Visit.create(isReverse, isCountPath)); - st.push(test.getArg(1)); - st.setTarget(start); - Node prev = stack.getStart(); - - eval(st, path, prev, src); - - if (st.isSuccess()) { - eval(stack, path, start, src); - } - - stack.push(exp); - } else if (test.retype() == Regex.OPTION) { - if (!stack.getVisit().nloop(test, start)) { - eval(stack, path, start, src); - } - - stack.push(exp); - } - } - - private void evalAlt(Regex exp, Record stack, Path path, Node start, Node src) { - stack.push(exp.getArg(0)); - eval(stack, path, start, src); - stack.pop(); - - stack.push(exp.getArg(1)); - eval(stack, path, start, src); - stack.pop(); - - stack.push(exp); - } - - private void evalLabel(Regex exp, Record stack, Path path, Node start, Node src) { - if (path.size() >= path.getMax()) { - stack.push(exp); - return; - } - - Node previous = null; - for (Edge ent : producer.getEdges(gNode, from, edge, memory, exp, src, start, index)) { - if (isStop || stack.isSuccess()) { - stack.push(exp); - return; - } - previous = processEdge(ent, exp, stack, path, start, src, previous); - } - - stack.push(exp); - } - - private Node processEdge(Edge ent, Regex exp, Record stack, Path path, Node start, Node src, Node previous) { - if (ent == null) { - return previous; - } - - Edge rel = ent; - Node node = rel.getNode(index); - if (exp.isInverse() || exp.isReverse()) { - EdgeInv ei = new EdgeInv(ent); - rel = ei; - ent = ei; - node = rel.getNode(index); - } - - if (filter != null && start == null && !test(rel.getNode(index))) { - return previous; - } - - Node currentSrc = src; - if (path.size() == 0 && src == null && gNode != null) { - currentSrc = ent.getGraph(); - } else if (currentSrc != null && !ent.getGraph().match(currentSrc)) { - return previous; - } - - Visit visit = stack.getVisit(); - boolean isStart = start == null; - if (isStart) { - previous = recordStartNode(node, previous, visit, stack); - } - - if (isShort && !checkShortestPath(visit, rel.getNode(other), exp, path.weight() + exp.getWeight())) { - return previous; - } - - stepPath(ent, rel, exp, stack, path, currentSrc); - - if (isStart) { - visit.nleave(node); - stack.popStart(); - } - - return previous; - } - - private Node recordStartNode(Node node, Node previous, Visit visit, Record stack) { - boolean isNew = previous == null || !previous.match(node); - if (isNew) { - visit.start(); - } - visit.nstart(node); - stack.pushStart(node); - if (isShort && isNew) { - producer.initPath(edge, 0); - visit.initPath(); - } - return node; - } - - private boolean checkShortestPath(Visit visit, Node otherNode, Regex exp, int length) { - Integer l = visit.getLength(otherNode, exp); - if (l == null) { - visit.setLength(otherNode, exp, length); - return true; - } - if (length > l || (isOne && length == l)) { - return false; - } - visit.setLength(otherNode, exp, length); - return true; - } - - private void stepPath(Edge ent, Edge rel, Regex exp, Record stack, Path path, Node currentSrc) { - if (hasListener) { - listener.enter(ent, exp, path.size()); - } - path.add(ent, exp.getWeight()); - boolean suc = kgram.getVisitor().step(kgram, currentSrc, edge, path, path.firstNode(), path.lastNode()); - if (suc) { - eval(stack, path, rel.getNode(other), currentSrc); - } - path.remove(exp.getWeight()); - if (hasListener) { - listener.leave(ent, exp, path.size()); - } - } - - boolean isDistinct(Record stack, Node start, Node target) { - boolean b = stack.getVisit().isDistinct(start, target); - if (b) { - stack.getVisit().addDistinct(start, target); - } - return b; - } - - void result(Record stack, Path path, Node start, Node src) { - if (path.size() > 0) { - if (isDistinct && !isDistinct(stack, path.firstNode(), path.lastNode())) { - // distinct (start,target) - return; - } - - boolean shouldStore = true; - if (hasListener) { - shouldStore = listener.process(path); - } - - if (shouldStore) { - Mapping map = result(path, gNode, src, start, isReverse); - if (map != null) { - result(src, map); - } - } - } else { - resultNode(path, start, src); - } - } - - void resultNode(Path path, Node start, Node src) { - for (Node node : getNodeIterator(start)) { - if (isStop) { - return; - } - - if (node != null) { - if (gNode != null) { - src = node.getGraph(); - } - Mapping m = result(path, gNode, src, node, isReverse); - - if (m != null) { - result(src, m); - } - } - } - } - - void result(Node src, Mapping map) { - kgram.getVisitor().path(kgram, src, edge, map.getPath(2), map.getNode(0), map.getNode(1)); - if (isList) { - lMap.add(map); - } else { - mbuffer.put(map, true); - } - } - - void option(Regex exp, Record stack, Path path, Node start, Node src) { - if (stack.getVisit().nloop(exp, start)) { - stack.push(exp); - return; - } - - // skip option - eval(stack, path, start, src); - - // with option - // push check: - stack.push(exp.getArg(1)); - stack.push(exp.getArg(0)); - eval(stack, path, start, src); - - // pop exp and check - stack.pop(); - stack.pop(); - stack.push(exp); - - stack.getVisit().nunset(exp); - } - - /** - * exp* - */ - void star(Regex exp, Record stack, Path path, Node start, Node src) { - // start is the first node of exp* - boolean isFirst = stack.getVisit().nfirst(exp); - - if (stack.getVisit().nloop(exp, start)) { - // start already met in exp path: stop - stack.push(exp); - return; - } - - // use case: (p*/q)* - // we must save each visited of p* - // because it expands to p*/q . p*/q ... - // and each occurrence of p* must have its own visited - Visit.VisitedNode save = stack.getVisit().nunset(exp); - eval(stack, path, start, src); - stack.getVisit().nset(exp, save); - - // restore exp* - stack.push(exp); - // push exp - stack.push(exp.getArg(0)); - // second step: eval exp once more - eval(stack, path, start, src); - // restore stack (exp* on top) - stack.pop(); - - stack.getVisit().nremove(exp, start); - if (isFirst) { - stack.getVisit().nunset(exp); - } - } - - /** - * exp = exp+ ; stack = rest - * Distinguish 1. first execution where index(exp+) = 0 and 2. next executions where index(exp+) = 1 - * 1. stack := (exp, exp+, rest) ; eval(stack) - * 2. a) stack = (rest) ; eval(stack) b) stack := (exp, exp+, rest) ; eval(stack) - * In addition there are two cases whether start node is bound or not - * If start is bound, OK - * If start is not bound, it will be bound by case LABEL: above - * ?x p+ ?y - * When index(p+) = 0, start is not bound, execute(p/p+). When we come back to p+ in the stack, - */ - void plus(Regex exp, Record stack, Path path, Node start, Node src) { - Visit visit = stack.getVisit(); - // start is the first node of exp+ - visit.nfirst(exp); - - if (visit.count(exp) == 0) { - // case 1: first execution of exp+ - - // declare exp such that when start node changes (in case LABEL:) - // the visitedNode table of exp be cleared by visit.start() - visit.declare(exp); - - // push exp+ again in stack to loop later - stack.push(exp); - // assign index=1 to exp+, hence exp+ will be executed by case 2 below - visit.count(exp, +1); - // push exp to execute it now - stack.push(exp.getArg(0)); - // execute exp ; stack = (exp, exp+, rest) ; exp+ will be executed by case 2 below - eval(stack, path, start, src); - stack.pop(); - visit.count(exp, -1); - - if (!isCountPath) { - // std sparql - // leave exp+ - visit.nremove(exp, start); - } - } else { - // case 2: - // next execution of exp+ after first one - if (visit.nloop(exp, start)) { - stack.push(exp); - return; - } - // we have executed exp at least once - // exp = exp+ ; stack = (rest) - // (1) evaluate the stack if any and store result - // use case: eval rest in: exp+ / rest - // switch off exp+ visitedNode table in case: (exp1+/exp2+)+ - // exp1 would be ready to start "again" with "new" visitedNode table - Visit.VisitedNode save = visit.nunset(exp); - visit.set(exp, 0); - - // eval rest - eval(stack, path, start, src); - - // switch on exp+ index and visitedNode table - visit.set(exp, 1); - visit.nset(exp, save); - - // (2) loop again - // push exp+ - stack.push(exp); - // push exp - stack.push(exp.getArg(0)); - // stack = (exp, exp+, rest) ; eval(stack) - eval(stack, path, start, src); - stack.pop(); - - visit.nremove(exp, start); - } - } - - /** - * exp{n,m} exp{n,} - */ - void count(Regex exp, Record stack, Path path, Node start, Node src) { - if (stack.getVisit().count(exp) >= exp.getMin()) { - countMinReached(exp, stack, path, start, src); - } else { - countMinNotReached(exp, stack, path, start, src); - } - } - - private void countMinReached(Regex exp, Record stack, Path path, Node start, Node src) { - if (checkLoop(exp) && stack.getVisit().nloop(exp, start)) { - stack.push(exp); - return; - } - - // min length is reached, can leave - int save = stack.getVisit().count(exp); - stack.getVisit().set(exp, 0); - eval(stack, path, start, src); - stack.getVisit().set(exp, save); - - stack.push(exp); - - if (stack.getVisit().count(exp) < exp.getMax()) { - // max length not reached, can continue - stack.getVisit().count(exp, +1); - stack.push(exp.getArg(0)); - eval(stack, path, start, src); - stack.pop(); - stack.getVisit().count(exp, -1); - } - - if (checkLoop(exp)) { - stack.getVisit().nremove(exp, start); - } - } - - private void countMinNotReached(Regex exp, Record stack, Path path, Node start, Node src) { - if (isReverse) { - if (checkLoop(exp)) { - // use case: ?x exp[2,] - // path goes backward - stack.getVisit().ninsert(exp, start); - } - } else if (checkLoop && stack.getVisit().nloop(exp, start)) { - stack.push(exp); - return; - } - - stack.push(exp); - - stack.getVisit().count(exp, +1); - stack.push(exp.getArg(0)); - eval(stack, path, start, src); - stack.pop(); - stack.getVisit().count(exp, -1); - - if (isReverse) { - if (checkLoop(exp)) { - // use case: ?x exp[2,] - // path goes backward - stack.getVisit().nremove(exp, start); - } - } else if (checkLoop) { - stack.getVisit().nremove(exp, start); - } - } - - boolean hasMax(Regex exp) { - return exp.getMax() != -1 && exp.getMax() != Integer.MAX_VALUE; - } - - // for count exp [n,m] - boolean checkLoop(Regex exp) { - return checkLoop || !hasMax(exp); - } -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PathMappingBuffer.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PathMappingBuffer.java deleted file mode 100644 index c94694f9b..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PathMappingBuffer.java +++ /dev/null @@ -1,65 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.path; - -import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; - -import java.util.Iterator; -import java.util.NoSuchElementException; - -/** - * Synchronized buffer to put/get path edges - * Edges are consumed by an iterator - * - * @author Olivier Corby, Edelweiss, INRIA 2010 - * - */ -// A producer/consumer stream is consumed once; replay would require unbounded buffering. -@SuppressWarnings("java:S4348") -public class PathMappingBuffer implements Iterable, Iterator { - - private Mapping map; - private boolean hasNext = true; - private boolean available = false; - - public synchronized Mapping next() { - if (!hasNext()) { - throw new NoSuchElementException("Path enumeration has ended"); - } - available = false; - notifyAll(); - return map; - } - - public synchronized boolean hasNext() { - while (!available) { - try { - wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return false; - } - } - notifyAll(); - return hasNext; - } - - public synchronized void put(Mapping val, boolean next) { - while (available) { - try { - wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - } - map = val; - hasNext = next; - available = true; - notifyAll(); - } - - @Override - public Iterator iterator() { - return this; - } - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PropertyPath.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PropertyPath.java index e6cdc5cae..af47cf172 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PropertyPath.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PropertyPath.java @@ -1,11 +1,28 @@ package fr.inria.corese.core.next.query.impl.engine.path; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopy; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopyable; import fr.inria.corese.core.next.query.impl.engine.model.Node; import java.util.List; /** Immutable logical property paths; independent of the SPARQL parser. */ -public sealed interface PropertyPath { +public sealed interface PropertyPath extends PlanCopyable { + /** Copies indexed nodes while preserving the logical path structure. */ + @Override + default PropertyPath copyPlan(PlanCopy copies) { + PropertyPath result = switch (this) { + case Predicate(var predicate) -> new Predicate(copies.copy(predicate)); + case Sequence(var left, var right) -> new Sequence(copies.copy(left), copies.copy(right)); + case Alternative(var left, var right) -> new Alternative(copies.copy(left), copies.copy(right)); + case Inverse(var operand) -> new Inverse(copies.copy(operand)); + case Repetition(var operand, var includeZero, var recursive) -> new Repetition(copies.copy(operand), includeZero, recursive); + case Negated(var forward, var backward) -> new Negated(copies.copy(forward), copies.copy(backward)); + }; + copies.remember(this, result); + return result; + } + record Predicate(Node predicate) implements PropertyPath { } record Sequence(PropertyPath left, PropertyPath right) implements PropertyPath { } record Alternative(PropertyPath left, PropertyPath right) implements PropertyPath { } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PropertyPathEdge.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PropertyPathEdge.java index 91eb16e14..19550b7e6 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PropertyPathEdge.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/PropertyPathEdge.java @@ -1,11 +1,22 @@ package fr.inria.corese.core.next.query.impl.engine.path; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopy; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopyable; import fr.inria.corese.core.next.query.impl.engine.model.Edge; import fr.inria.corese.core.next.query.impl.engine.model.KgramNodes; import fr.inria.corese.core.next.query.impl.engine.model.Node; /** Logical binary relation carried through the engine's ordinary edge joins. */ -public record PropertyPathEdge(Node subject, PropertyPath path, Node object, Node graph) implements Edge { +public record PropertyPathEdge(Node subject, PropertyPath path, Node object, Node graph) implements Edge, PlanCopyable { + /** Copies the path relation and all of its indexed nodes. */ + @Override + public PropertyPathEdge copyPlan(PlanCopy copies) { + PropertyPathEdge result = new PropertyPathEdge(copies.copy(subject), copies.copy(path), + copies.copy(object), copies.copy(graph)); + copies.remember(this, result); + return result; + } + @Override public Node getNode(int index) { return switch (index) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/Record.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/Record.java deleted file mode 100644 index 3b6b88516..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/Record.java +++ /dev/null @@ -1,93 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.path; - -import fr.inria.corese.core.next.query.impl.engine.model.Node; -import fr.inria.corese.core.next.query.impl.engine.model.Regex; - -import java.util.ArrayList; - -/** - * Stack of Regex + Visit for loop check - * @author Olivier Corby, Edelweiss, INRIA 2011 - * - */ -// Execution metadata does not participate in List equality or serialization. -@SuppressWarnings("java:S2160") -public class Record extends ArrayList { - - transient Visit visit; - transient Node target; - - transient ArrayList stack; - - boolean success = false; - - Record(Visit v){ - visit = v; - stack = new ArrayList<>(); - } - - - Visit getVisit(){ - return visit; - } - - - void push(Regex exp){ - if (exp != null){ - add(exp); - } - } - - - Regex pop(){ - if (size() == 0){ - return null; - } - Regex exp = get(size()-1); - remove(size()-1); - return exp; - } - - - /************************************************************* - * Stack dedicated to PARA: e1 || e2 - * e1 goes from start to target - * then e2 checks that there is a path from start to target - * pushStart() store start node - * setTarget() store target node in a fresh new Record, - * hence there is no stack for target - * New Record is created for loop check specific to e2 path - * - *************************************************************/ - - void pushStart(Node start){ - stack.add(start); - } - - Node getStart(){ - return stack.getLast(); - } - - void popStart(){ - stack.getLast(); - stack.removeLast(); - } - - void setTarget(Node n){ - target = n; - } - - Node getTarget(){ - return target; - } - - void setSuccess(){ - success = true; - } - - boolean isSuccess(){ - return success; - } - - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/Visit.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/Visit.java deleted file mode 100644 index fabd47d26..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/path/Visit.java +++ /dev/null @@ -1,425 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.path; - -import fr.inria.corese.core.next.query.impl.engine.model.Node; -import fr.inria.corese.core.next.query.impl.engine.model.Regex; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.TreeMap; - -/** - * Record intermediate nodes that are visited by a loop path expression such as - * exp+ exp* exp{n,} Prevent loop among intermediate nodes - * - * @author Olivier Corby, Edelweiss, INRIA 2010 - * - */ -public class Visit { - int nextVisitId = 0; - - ExpVisitedNode visitedNode; - HashMap ctable; - HashMap startNode; - DTable tdistinct; - LTable ltable; - ArrayList regexList; - - boolean isReverse; - // isCounting = false: SPARQL semantics without path counting - boolean isCounting; - - Visit(boolean isRev, boolean isCount) { - isCounting = isCount; - isReverse = isRev && isCount; - - visitedNode = new ExpVisitedNode(isReverse); - startNode = new HashMap<>(); - ctable = new HashMap<>(); - regexList = new ArrayList<>(); - tdistinct = new DTable(); - ltable = new LTable(); - } - - static class NodeTable extends TreeMap { - - NodeTable() { - super(new Compare()); - } - } - - // Traversal direction is metadata; equality must retain the Map contract. - @SuppressWarnings("java:S2160") - class ExpVisitedNode extends HashMap { - boolean reverse; - - ExpVisitedNode(boolean b) { - reverse = b; - } - - void add(Regex exp, Node n) { - VisitedNode t = get(exp); - if (t == null) { - t = new VisitedNode(reverse, nextVisitId++); - put(exp, t); - } - t.add(n); - } - - void remove(Regex exp, Node n) { - VisitedNode t = get(exp); - t.remove(n); - } - - boolean loop(Regex exp, Node start) { - VisitedNode table = get(exp); - if (table == null) { - return false; - } - return table.loop(exp, start); - } - - boolean exist(Regex exp) { - return containsKey(exp); - } - - } - - static class VisitedNode { - - NodeTable table; - List list; - boolean isReverse; - int n; - - - VisitedNode(boolean b, int n) { - this.n = n; - isReverse = b; - if (isReverse) { - list = new ArrayList<>(); - } else { - table = new NodeTable(); - } - } - - void clear() { - if (table != null) table.clear(); - if (list != null) list.clear(); - } - - @Override - public String toString() { - if (table == null) { - return list.toString(); - } - return table.toString(); - } - - - void add(Node n) { - if (isReverse) { - list.add(n); - } else { - table.put(n, n); - } - } - - void remove(Node n) { - if (isReverse) { - list.removeLast(); - } else { - table.remove(n); - } - } - - boolean loop(Regex exp, Node n) { - if (isReverse) { - // @deprecated, used for count only - if (list == null || list.size() < 2) { - return false; - } - int last = list.size() - 1; - - // use case: ?s {2,} ex:uri - // Walk through the list backward (from to ?s), skip 2 first nodes - // cf W3C pp test case - Node node = null; - int count = 0; - int min = exp.getMin(); - if (min == -1) { - min = 0; - } - - for (int i = last; i >= 0; i--) { - if (count == min) { - node = list.get(i); - } else if (list.get(i).equals(node)) { - return true; - } - count++; - } - - return false; - } else { - return table.containsKey(n); - } - } - - } - - static class DTable extends TreeMap { - - DTable() { - super(new Compare()); - } - } - - static class Compare implements Comparator { - - // Use the node ordering consistently for loop detection. - public int compare(Node o1, Node o2) { - return o1.compare(o2); - } - } - - static Visit create(boolean isReverse, boolean isCount) { - return new Visit(isReverse, isCount); - } - - - /* - * ************************************ - * With Regex - * - ************************************* - */ - /** - * Test whether path loops - */ - boolean nloop(Regex exp, Node start) { - if (isReverse) { - ninsert(exp, start); - if (visitedNode.loop(exp, start)) { - visitedNode.remove(exp, start); - return true; - } - } else if (visitedNode.loop(exp, start)) { - return true; - } else { - ninsert(exp, start); - } - - return false; - } - - VisitedNode getVisitedNode(Regex exp) { - return visitedNode.get(exp); - } - - - - boolean nfirst(Regex exp) { - return !visitedNode.exist(exp); - } - - void ninsert(Regex exp, Node start) { - if (start == null) { - // subscribe exp to start() above because start node is not bound yet - declare(exp); - } else { - visitedNode.add(exp, start); - } - } - - void declare(Regex exp) { - if (!regexList.contains(exp)) { - regexList.add(exp); - } - } - - void undeclare(Regex exp) { - regexList.remove(exp); - } - - boolean knows(Regex exp) { - return regexList.contains(exp); - } - - void nset(Regex exp, VisitedNode t) { - if (t != null) { - visitedNode.put(exp, t); - } - } - - - VisitedNode nunset(Regex exp) { - VisitedNode t = visitedNode.get(exp); - visitedNode.remove(exp); - return t; - } - - /** - * use case: ?x p+ ?y - * ?x is not bound when path starts first occurrence of p - * binds ?x node is ?x exp is p+ - */ - void nstart(Node node) { - for (Regex exp : regexList) { - nstart(exp, node); - } - } - - void nstart(Regex exp, Node node) { - if (exp.retype() != Regex.PLUS) { - visitedNode.add(exp, node); - } - } - - /** - * same use case as nstart() above leave the binding - */ - void nleave(Node node) { - for (Regex exp : regexList) { - nleave(exp, node); - } - } - - void nleave(Regex exp, Node node) { - if (exp.retype() != Regex.PLUS) { - visitedNode.remove(exp, node); - } - } - - /** - * Remove node from visited list - */ - void nremove(Regex exp, Node node) { - if (node == null) { - undeclare(exp); - return; - } - if (isCounting) { - visitedNode.remove(exp, node); - } - } - - void set(Regex exp, int n) { - ctable.put(exp, n); - } - - void count(Regex exp, int n) { - Integer i = ctable.get(exp); - if (i == null) { - i = 0; - } - ctable.put(exp, i + n); - } - - int count(Regex exp) { - Integer i = ctable.get(exp); - if (i == null) { - return 0; - } - return i; - } - - /** - * ************************************** - * ?x distinct(exp+) ?y - * - */ - boolean isDistinct(Node start, Node node) { - NodeTable t = tdistinct.get(start); - if (t == null) { - return true; - } - return !t.containsKey(node); - } - - void addDistinct(Node start, Node node) { - NodeTable t = tdistinct.computeIfAbsent(start, key -> new NodeTable()); - t.put(node, node); - } - - /* - * ******************************************** - * ?x short(regex) ?y ?x distinct(regex) ?y - * - */ - /** - * Table for managing path length for each node - */ - static class LTable extends TreeMap { - - LTable() { - super(new Compare()); - } - - void setLength(Node n, Regex exp, int l) { - Record r = get(n); - if (r == null) { - r = new Record(exp, l); - put(n, r); - } else if (r.exp == exp) { - r.len = l; - } - } - - Integer getLength(Node n, Regex exp) { - Record r = get(n); - if (r != null && r.exp == exp) { - return r.len; - } else { - return null; - } - } - } - - static class Record { - - Regex exp; - Integer len; - - Record(Regex e, Integer l) { - exp = e; - len = l; - } - - } - - void initPath() { - ltable.clear(); - } - - /** - * New start node: clean the table of visited nodes - */ - void start() { - if (!isCounting) { - clearall(); - } - } - - - /** - * New start node: clean the table of visited nodes - */ - void clearall() { - for (Regex exp : regexList) { - VisitedNode t = getVisitedNode(exp); - if (t != null) { - t.clear(); - } - } - } - - void setLength(Node n, Regex exp, int l) { - ltable.setLength(n, exp, l); - } - - Integer getLength(Node n, Regex exp) { - return ltable.getLength(n, exp); - } - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/BgpGenerator.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/BgpGenerator.java deleted file mode 100644 index 5ade9ec1d..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/BgpGenerator.java +++ /dev/null @@ -1,7 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.pattern; - -public interface BgpGenerator { - - Exp process(Exp exp); - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/CompiledQueryPlan.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/CompiledQueryPlan.java new file mode 100644 index 000000000..78ccedfb7 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/CompiledQueryPlan.java @@ -0,0 +1,56 @@ +package fr.inria.corese.core.next.query.impl.engine.pattern; + +import fr.inria.corese.core.next.query.impl.engine.diagnostics.ExecutionDiagnostics; +import fr.inria.corese.core.next.query.impl.engine.model.Edge; +import java.util.List; +import java.util.Map; + +/** + * Immutable compilation image, enforced by exclusive ownership rather than by + * reusing an executable Query. Capture detaches every mutable component from + * the compiler. The private image is never evaluated or exposed; each execution + * receives another independent graph. Only immutable syntax, RDF values and + * private read-only prologue lookups are shared. No producer, storage cursor, + * result mappings, initial bindings or execution context is retained. + */ +public final class CompiledQueryPlan { + private final Query image; + private final List estimates; + + private CompiledQueryPlan(Query query, Map counts) { + PlanCopy copies = new PlanCopy(); + image = copies.copy(query); + estimates = counts.entrySet().stream() + .map(entry -> new Estimate(copies.copy(entry.getKey()), entry.getValue())).toList(); + } + + /** + * Captures a completed query without retaining any compiler-owned mutable object. + * + * @param query completed native query + * @param counts estimates obtained while ordering its patterns + * @return immutable compilation image + */ + public static CompiledQueryPlan capture(Query query, Map counts) { + return new CompiledQueryPlan(query, counts); + } + + /** + * Creates independent runtime containers without AST translation or planning. + * + * @param diagnostics optional request-scoped collector, or null + * @return completed query owned by one execution + */ + public Query newExecution(ExecutionDiagnostics diagnostics) { + PlanCopy copies = new PlanCopy(); + Query runtime = copies.copy(image); + if (diagnostics != null) { + for (Estimate estimate : estimates) { + diagnostics.estimated(copies.copy(estimate.edge()), estimate.cardinality()); + } + } + return runtime; + } + + private record Estimate(Edge edge, long cardinality) { } +} 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 c57ebc781..576bff7e1 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 @@ -12,23 +12,27 @@ import fr.inria.corese.core.next.query.impl.engine.model.Filter; import fr.inria.corese.core.next.query.impl.engine.model.Node; import fr.inria.corese.core.next.query.impl.engine.model.PointerType; -import fr.inria.corese.core.next.query.impl.engine.model.Regex; +import fr.inria.corese.core.next.query.impl.engine.path.PropertyPathEdge; import fr.inria.corese.core.next.query.impl.engine.spi.Producer; import java.util.ArrayList; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import static fr.inria.corese.core.next.query.impl.engine.model.PointerType.STATEMENT; /** - * KGRAM/SPARQL expressions: bgp, union, optional, etc. + * Mutable operators for compiled SPARQL graph patterns and solution modifiers. + * + *

    Child operators, indexed nodes and attached expressions belong to one + * compiler or evaluation. {@link #copyPlan(PlanCopy)} detaches mutable links + * while preserving their identity relationships within the copied graph. + * An executable graph must not be shared between concurrent evaluations.

    * * @author Olivier Corby, Edelweiss, INRIA 2010 */ public class Exp extends PointerObject - implements ExpType, ExpPattern, Iterable { + implements ExpType, ExpPattern, Iterable, PlanCopyable { public static final int SUBJECT = 0; public static final int OBJECT = 1; @@ -37,43 +41,34 @@ public class Exp extends PointerObject static final String NEWLINE = "\n"; static final String SP = " "; Type type; - int index = -1; + private int bindingIndex = -1; // optional success boolean status = false; - boolean skip; boolean isFail = false; - boolean isPath = false; boolean isAggregate = false; boolean isSilent = false; VExp args; Edge edge; Node node; List lNodes; - // Filter api refer to sparql.triple.parser.Expression + // Expression evaluation contract, independent of parser syntax types. Filter filter; List lFilter; // min(?l, expGroupBy(?x, ?y)) List expGroupBy; // for UNION Stack stack; - // for EXTERN + // Optimizer-specific payload, such as candidate values for an equality filter. Object payload; Producer producer; - Regex regex; Mappings map; - HashMap mappingCache; - int min = -1; - int max = -1; private boolean isPostpone = false; - private boolean bgpAble = false; private boolean isFunctional = false; private boolean generated = false; - private Node arg; // service with several URI: private List simpleNodeList; private Exp postpone; private List inscopeFilter; - private Exp path; private Exp bind; private Exp values; private int level = -1; @@ -89,8 +84,51 @@ public class Exp extends PointerObject lFilter = new ArrayList<>(); } + /** Copies this operator and all mutable links into an independent execution graph. */ + @Override + public Exp copyPlan(PlanCopy copies) { + Exp target = Exp.create(type); + copies.remember(this, target); + copyPlanFields(target, copies); + return target; + } + + /** Copies the common operator fields after the target has been registered. */ + void copyPlanFields(Exp target, PlanCopy copies) { + target.type = type; + target.bindingIndex = bindingIndex; + target.status = status; + target.isFail = isFail; + target.isAggregate = isAggregate; + target.isSilent = isSilent; + target.edge = copies.copy(edge); + target.node = copies.copy(node); + target.lNodes = copies.copy(lNodes); + target.filter = copies.copy(filter); + target.lFilter = copies.copy(lFilter); + target.expGroupBy = copies.copy(expGroupBy); + target.stack = copies.copy(stack); + target.payload = copies.copy(payload); + target.producer = copies.copy(producer); + target.map = copies.copy(map); + target.isPostpone = isPostpone; + target.isFunctional = isFunctional; + target.generated = generated; + target.simpleNodeList = copies.copy(simpleNodeList); + target.postpone = copies.copy(postpone); + target.inscopeFilter = copies.copy(inscopeFilter); + target.bind = copies.copy(bind); + target.values = copies.copy(values); + target.level = level; + target.number = number; + target.num = num; + target.isSystem = isSystem; + target.mappings = mappings; + for (Exp child : args) target.args.add(copies.copy(child)); + } + public static Exp create(Type t) { - if (t == Type.PATH || t == Type.EDGE) { + if (t == Type.EDGE) { return new ExpEdge(t); } return new Exp(t); @@ -154,14 +192,6 @@ public void setFunctional(boolean isFunctional) { this.isFunctional = isFunctional; } - public Exp getPath() { - return path; - } - - public boolean hasPath() { - return path != null; - } - public boolean isSystem() { return isSystem; } @@ -186,22 +216,6 @@ public boolean isBGPAnd() { return type == Type.AND || type == Type.BGP; } - public boolean isBGPAble() { - return bgpAble; - } - - public void setBGPAble(boolean bgpAble) { - this.bgpAble = bgpAble; - } - - public Node getCacheNode() { - return arg; - } - - public void setCacheNode(Node arg) { - this.arg = arg; - } - public Exp getValues() { return values; } @@ -210,7 +224,7 @@ public boolean isPostpone() { return isPostpone; } - // draft for BGP + /** Returns a shallow operator containing the same child references. */ public Exp duplicate() { Exp exp = Exp.create(type()); for (Exp e : this) { @@ -372,11 +386,11 @@ public boolean isStatement() { } public int getIndex() { - return index; + return bindingIndex; } public void setIndex(int n) { - index = n; + bindingIndex = n; } public boolean isBGPFilter() { @@ -421,7 +435,7 @@ public boolean isEdge() { } public boolean isEdgePath() { - return isEdge() || isPath(); + return isEdge(); } public boolean isOption() { @@ -516,14 +530,6 @@ public void setEdge(Edge e) { edge = e; } - public Regex getRegex() { - return regex; - } - - public void setRegex(Regex f) { - regex = f; - } - @Override public Mappings getMappings() { return map; @@ -589,11 +595,7 @@ public void setFail(boolean b) { } public boolean isPath() { - return type == Type.PATH; - } - - public void setPath(Exp path) { - this.path = path; + return isEdge() && getEdge() instanceof PropertyPathEdge; } public boolean isValues() { @@ -671,21 +673,6 @@ public List getObjectValues() { return new ArrayList<>(); } - public int getMin() { - return min; - } - - public void setMin(int i) { - min = i; - } - - public int getMax() { - return max; - } - - public void setMax(int i) { - max = i; - } public Stack getStack() { return stack; @@ -707,7 +694,7 @@ boolean distinct(Node qNode) { for (int i = 0; i < size(); i++) { Exp exp = get(i); switch (exp.type()) { - case EDGE, PATH, XPATH, EVAL: + case EDGE, XPATH, EVAL: if (exp.contains(qNode)) { add(i + 1, Exp.create(Type.ACCEPT, qNode)); return true; @@ -730,12 +717,12 @@ boolean distinct(Node qNode) { } public boolean isSortable() { - return isEdge() || isPath() || isGraph() || type == Type.OPT_BIND; + return isEdge() || isGraph() || type == Type.OPT_BIND; } boolean isSimple() { return switch (type) { - case EDGE, PATH, EVAL, OPT_BIND -> true; + case EDGE, EVAL, OPT_BIND -> true; default -> false; }; } @@ -758,18 +745,6 @@ public boolean bind(Mapping map) { return false; } - /** - * check special case: e1: ?x path ?y e2: graph path {} e2 cannot be moved - * before e1 - */ - public boolean isGraphPath(Exp e1, Exp e2) { - if (e1.isPath() && e2.isGraph()) { - Node var1 = e1.getEdge().getEdgeVariable(); - Node var2 = e2.getGraphName(); - return var1 != null && var2.isVariable() && var1.same(var2); - } - return false; - } public int nBind(List lNode, List lVar, List lBind) { if (isSimple()) { @@ -820,7 +795,7 @@ boolean member(Node node, List lBind) { /** * list of nodes that are bound by this exp no minus and no exists - * use case: Sorter + * use case: ConnectedPatternOrdering */ public void bind(List lNode) { if (isSimple()) { @@ -846,7 +821,7 @@ void bind(Node node, List lNode) { public void addBind(List lVar) { Type title = type(); - if (title == Type.EDGE || title == Type.PATH) { + if (title == Type.EDGE) { for (int i = 0; i < nbNode(); i++) { Node bindNode = getNode(i); addBind(bindNode, lVar); @@ -865,7 +840,7 @@ void addBind(Node node, List lVar) { */ int nbNode() { switch (type) { - case EDGE, PATH, EVAL: + case EDGE, EVAL: if (edge.getEdgeVariable() == null) { return edge.nbNode(); } else { @@ -885,7 +860,7 @@ int nbNode() { */ Node getNode(int n) { switch (type) { - case EDGE, PATH, EVAL: + case EDGE, EVAL: if (n < edge.nbNode()) { return edge.getNode(n); } else { @@ -945,7 +920,7 @@ public void share(List filterVar, List expVar) { share(getNode(), filterVar, expVar); break; - case EDGE, PATH: + case EDGE: for (int i = 0; i < nbNode(); i++) { Node nodePath = getNode(i); share(nodePath, filterVar, expVar); @@ -1025,7 +1000,7 @@ void getNodes(ExpNodeCollector h) { h.add(getNode()); break; - case EDGE, PATH: + case EDGE: collectEdgeNodes(h); break; @@ -1171,7 +1146,7 @@ void queryNodeList(ExpNodeCollector h) { * For modularity reasons, Pattern is stored as ExpPattern interface */ public Exp getPattern(Expr exp) { - return (Exp) exp.getPattern(); + return exp.getPattern(); } /** @@ -1335,7 +1310,7 @@ private void processBind(int index, Exp bindExp) { } if (j >= 0) { Exp g = get(j); - if ((g.isEdge() || g.isPath()) + if (g.isEdge() && (!bindExp.isBindCst() || g.bind(bindExp.first().getNode()))) { bindExp.status(true); g.setBind(bindExp); @@ -1355,18 +1330,6 @@ boolean bind(Node node) { return false; } - public boolean hasCache() { - return mappingCache != null; - } - - public void cache(Node n, Mappings m) { - mappingCache.put(n, m); - } - - public Mappings getMappings(Node n) { - return mappingCache.get(n); - } - /** * {?x ex:p ?y} optional {?y ex:q ?z filter(?z != ?x) optional {?z ?p ?x}} * filter ?x is not bound in optional ... @@ -1459,7 +1422,7 @@ List getNodeVariables() { List list = new ArrayList<>(); for (Exp exp : this) { switch (exp.type()) { - case EDGE, PATH: + case EDGE: exp.getEdgeVariables(list); break; case BIND: diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpEdge.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpEdge.java index 8aa699fe3..704c5e088 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpEdge.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpEdge.java @@ -28,6 +28,7 @@ public class ExpEdge extends Exp { * - oper = type or oper = boolean connector on such subexp */ @Override + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. public List getFilters(int node, int type){ Node n = getNode(node); if (n == null || ! n.isVariable()){ diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpNodeCollector.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpNodeCollector.java index 3bd266097..2eff4d976 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpNodeCollector.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpNodeCollector.java @@ -8,7 +8,7 @@ /** * */ -public class ExpNodeCollector { +public final class ExpNodeCollector { private final List nodeList; private final List selectNodeList; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/Group.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/Group.java index 14b34512f..57b8f7c41 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/Group.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/Group.java @@ -34,7 +34,7 @@ public Group() { } public Group(List list) { - setNodeList(list); + this.nodes = list; table = new TreeMapping(list); } @@ -46,6 +46,7 @@ public static Group create(List lNode) { return new Group(lNode); } + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. public static Group createFromExp(List list) { List nodes = new ArrayList<>(); for (Exp exp : list) { @@ -133,6 +134,9 @@ public Iterable getValues() { } class TreeMapping extends TreeMap { + @java.io.Serial + private static final long serialVersionUID = 1L; + TreeMapping(List ln) { super(new Compare(ln)); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/PlanCopy.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/PlanCopy.java new file mode 100644 index 000000000..cc9f12dfe --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/PlanCopy.java @@ -0,0 +1,70 @@ +package fr.inria.corese.core.next.query.impl.engine.pattern; + +import fr.inria.corese.core.next.data.api.model.DatatypeValue; +import fr.inria.corese.core.next.query.impl.engine.eval.Stack; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +/** + * Identity-preserving copy of a completed native query graph. A copy operation is + * confined to one thread and discarded after construction. Unknown mutable types + * fail explicitly rather than being shared between the image and an execution. + */ +public final class PlanCopy { + private final IdentityHashMap copied = new IdentityHashMap<>(); + + /** + * Registers a shell before following cyclic graph links. + * + * @param source original object + * @param target independent copy under construction + */ + public void remember(Object source, Object target) { + copied.put(source, target); + } + + /** + * Copies a compiled object, preserving sharing only within the new graph. + * + * @param source contract type + * @param source object or null + * @return independent copy, or the same immutable scalar/value + * @throws IllegalArgumentException if the graph contains an unsupported mutable type + */ + @SuppressWarnings({"unchecked", "java:S9391"}) // Preserve source contracts and avoid a stream allocation for every copied container. + public T copy(T source) { + if (source == null || source instanceof String || source instanceof Enum + || source instanceof Integer || source instanceof Long || source instanceof Boolean || source instanceof DatatypeValue) { + return source; + } + Object existing = copied.get(source); + if (existing != null) { + return (T) existing; + } + if (source instanceof PlanCopyable value) { + return (T) value.copyPlan(this); + } + if (source instanceof Stack stack) { + Stack result = new Stack(); + remember(source, result); + for (Exp value : stack) result.add(copy(value)); + return (T) result; + } + if (source instanceof List list) { + List result = new ArrayList<>(list.size()); + remember(source, result); + for (Object value : list) result.add(copy(value)); + return (T) result; + } + if (source instanceof Map map) { + Map result = new HashMap<>(); + remember(source, result); + map.forEach((key, value) -> result.put(copy(key), copy(value))); + return (T) result; + } + throw new IllegalArgumentException("Unsupported mutable plan component: " + source.getClass().getName()); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/PlanCopyable.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/PlanCopyable.java new file mode 100644 index 000000000..f41d0984c --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/PlanCopyable.java @@ -0,0 +1,18 @@ +package fr.inria.corese.core.next.query.impl.engine.pattern; + +/** + * Internal graph-copy contract for compiled operators and execution-local adapters. + * Implementations register the new object before copying links that may form cycles. + * Immutable RDF values and syntax nodes may be shared; mutable containers may not. + * + * @param copied implementation type + */ +public interface PlanCopyable { + /** + * Copies this object in an identity-preserving graph-copy operation. + * + * @param copies current copy operation + * @return independent copy registered with the supplied operation + */ + T copyPlan(PlanCopy copies); +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/Query.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/Query.java index 705f2b5f0..a14803a05 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/Query.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/pattern/Query.java @@ -4,12 +4,11 @@ import fr.inria.corese.core.next.query.impl.engine.model.ExprType; import fr.inria.corese.core.next.query.impl.engine.model.Node; import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; -import fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorter; +import fr.inria.corese.core.next.query.impl.engine.planning.QueryPlanner; import fr.inria.corese.core.next.query.impl.engine.model.Filter; -import fr.inria.corese.core.next.query.impl.engine.spi.Matcher; import fr.inria.corese.core.next.query.impl.engine.spi.Producer; -import fr.inria.corese.core.next.query.impl.engine.filter.Compile; +import fr.inria.corese.core.next.query.impl.engine.filter.FilterOptimizer; import fr.inria.corese.core.next.query.impl.engine.eval.Message; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,13 +18,16 @@ import java.util.List; /** - * KGRAM Query - * also used for subquery + * Compiled query operators, projection, dataset and variable indexes. + * + *

    The compiler completes one mutable graph, including its subqueries, before + * execution. {@link CompiledQueryPlan} captures a detached compilation image and + * creates an independent graph for each evaluation. A query and its operators + * belong to that evaluation thread; they are not shared mutable execution state.

    * * @author Olivier Corby, Edelweiss, INRIA 2009 * */ -@SuppressWarnings({"java:S1845", "java:S1700", "java:S2387"}) // Legacy KGRAM AST structure public final class Query extends Exp { @@ -39,15 +41,13 @@ public final class Query extends Exp { private static final Logger logger = LoggerFactory.getLogger(Query.class); - public static final String PATHNODE = "pathNode"; - int limit = Integer.MAX_VALUE; int offset = 0; // if slice > 0 : service gets mappings from previous pattern by slices int slice = 20; private int number = 0; - boolean distinct = false; + private boolean distinctResults; int iNode = 0; int iEdge = 0; List from; @@ -71,7 +71,6 @@ public final class Query extends Exp { List orderBy; List groupBy; List failure; - List pathFilter; List funList; List errors; @@ -79,29 +78,21 @@ public final class Query extends Exp { Exp having; Exp construct; Exp delete; - // gNode is a local graph node when subquery has no ?g in its select + // gNode is a local graph node when subquery has no ?g in its select // use case: graph ?g (select where ...) Node gNode; - Node pathNode; - // outer main query that contains this (when subquery) - // SPIN graph - Query query; + // Root query whose dataset is inherited by this subquery. + private Query globalQuery; Query outerQuery; - Object object; + private Object queryObject; - // current transformer if any - private Object transformer; - // table: transformation -> Transformer - // shared by templates of Transformer - HashMap tprinter; - - Compile compiler; - private final QuerySorter querySorter; + FilterOptimizer compiler; + private final QueryPlanner querySorter; HashMap pragma; // Extended filters: pathNode() HashMap ftable; - // Extended queries for type check + // Extended queries for type check // nb occurrences of predicates in where HashMap ptable; HashMap etable; @@ -116,30 +107,20 @@ public final class Query extends Exp { boolean isCheck = false; private boolean isUseBind = true; - boolean - isAggregate = false; + private boolean aggregateQuery; boolean isRelax = false; boolean isDistribute = false; boolean isOptimize = false; boolean isTest = false; boolean isNew = true; - boolean isSort = true; boolean isConstruct = false; private boolean isInsert = false; boolean isDelete = false; boolean isUpdate = false; boolean isAsk = false; - boolean isCheckLoop = false; boolean isListGroup = false; - boolean isListPath = true; - private boolean parallel = true; private boolean validate = false; private boolean federate = false; - // store the list of edges of the path - private boolean isStorePath = true; - // cache PP result in PathFinder - private boolean isCachePath = false; - boolean isCountPath = false; boolean isCorrect = true; @@ -147,63 +128,136 @@ public final class Query extends Exp { boolean isMap = true; boolean isRule = false; boolean isDetail = true; - private boolean algebra = false; - int mode = Matcher.UNDEF; int planner = QP_DEFAULT; private int queryProfile = STD_PROFILE; private boolean isService = false; - private boolean isSynchronized = false; - private boolean lock = true; - private boolean isTemplate = false; - private boolean isTransformationTemplate = false; - - // member of a set of templates of a pprinter (not a single query that is a template) - - private Exp templateGroup; private final List argList; private Mapping mapping; - private boolean isExtension = false; - private BgpGenerator bgpGenerator; private String service; - public Query(){ + /** Creates an uncompiled query whose mutable containers belong to its compiler. */ + public Query() { super(Type.QUERY); - from = new ArrayList<>(); - named = new ArrayList<>(); - selectExp = new ArrayList<>(); - orderBy = new ArrayList<>(); - groupBy = new ArrayList<>(); - failure = new ArrayList<>(); - pathFilter = new ArrayList<>(); - funList = new ArrayList<>(); - - compiler = new Compile(this); - table = new HashMap<>(); - ftable = new HashMap<>(); - pragma = new HashMap<>(); - tprinter = new HashMap<> (); - ptable = new HashMap<>(); - etable = new HashMap<>(); - queries = new ArrayList<>(); - - selectNode = new ArrayList<>(); - patternNodes = new ArrayList<>(); - queryNodes = new ArrayList<>(); - patternSelectNodes = new ArrayList<>(); - querySelectNodes = new ArrayList<>(); - bindingNodes = new ArrayList<>(); - relaxEdges = new ArrayList<>(); - argList = new ArrayList<>(); - querySorter = new QuerySorter(this); + from = new ArrayList<>(); + named = new ArrayList<>(); + selectExp = new ArrayList<>(); + orderBy = new ArrayList<>(); + groupBy = new ArrayList<>(); + failure = new ArrayList<>(); + funList = new ArrayList<>(); + compiler = new FilterOptimizer(this); + table = new HashMap<>(); + ftable = new HashMap<>(); + pragma = new HashMap<>(); + ptable = new HashMap<>(); + etable = new HashMap<>(); + queries = new ArrayList<>(); + selectNode = new ArrayList<>(); + patternNodes = new ArrayList<>(); + queryNodes = new ArrayList<>(); + patternSelectNodes = new ArrayList<>(); + querySelectNodes = new ArrayList<>(); + bindingNodes = new ArrayList<>(); + relaxEdges = new ArrayList<>(); + argList = new ArrayList<>(); + querySorter = new QueryPlanner(this); + } + /** + * Constructs a completed execution graph without allocating compilation services. + * Registration precedes graph links because subqueries refer to their outer query. + */ + @SuppressWarnings("this-escape") // PlanCopy only records identity; no execution observes the shell before construction finishes. + private Query(Query source, PlanCopy copies) { + super(Type.QUERY); + copies.remember(source, this); + source.copyPlanFields(this, copies); + querySorter = null; + compiler = null; + argList = copies.copy(source.argList); + this.limit = source.limit; + this.offset = source.offset; + this.slice = source.slice; + this.number = source.number; + this.distinctResults = source.distinctResults; + this.iNode = source.iNode; + this.iEdge = source.iEdge; + this.from = copies.copy(source.from); + this.named = copies.copy(source.named); + this.selectNode = copies.copy(source.selectNode); + this.patternNodes = copies.copy(source.patternNodes); + this.patternSelectNodes = copies.copy(source.patternSelectNodes); + this.queryNodes = copies.copy(source.queryNodes); + this.querySelectNodes = copies.copy(source.querySelectNodes); + this.bindingNodes = copies.copy(source.bindingNodes); + this.constructNodes = copies.copy(source.constructNodes); + this.relaxEdges = copies.copy(source.relaxEdges); + this.selectExp = copies.copy(source.selectExp); + this.orderBy = copies.copy(source.orderBy); + this.groupBy = copies.copy(source.groupBy); + this.failure = copies.copy(source.failure); + this.funList = copies.copy(source.funList); + this.errors = copies.copy(source.errors); + this.info = copies.copy(source.info); + this.having = copies.copy(source.having); + this.construct = copies.copy(source.construct); + this.delete = copies.copy(source.delete); + this.gNode = copies.copy(source.gNode); + this.globalQuery = copies.copy(source.globalQuery); + this.outerQuery = copies.copy(source.outerQuery); + this.queryObject = copies.copy(source.queryObject); + this.pragma = copies.copy(source.pragma); + this.ftable = copies.copy(source.ftable); + this.ptable = copies.copy(source.ptable); + this.etable = copies.copy(source.etable); + this.table = copies.copy(source.table); + this.queries = copies.copy(source.queries); + this.isCompiled = source.isCompiled; + this.datasetSpecified = source.datasetSpecified; + this.reportEnabled = source.reportEnabled; + this.isCheck = source.isCheck; + this.isUseBind = source.isUseBind; + this.aggregateQuery = source.aggregateQuery; + this.isRelax = source.isRelax; + this.isDistribute = source.isDistribute; + this.isOptimize = source.isOptimize; + this.isTest = source.isTest; + this.isNew = source.isNew; + this.isConstruct = source.isConstruct; + this.isInsert = source.isInsert; + this.isDelete = source.isDelete; + this.isUpdate = source.isUpdate; + this.isAsk = source.isAsk; + this.isListGroup = source.isListGroup; + this.validate = source.validate; + this.federate = source.federate; + this.isCorrect = source.isCorrect; + this.isConnect = source.isConnect; + this.isMap = source.isMap; + this.isRule = source.isRule; + this.isDetail = source.isDetail; + this.planner = source.planner; + this.queryProfile = source.queryProfile; + this.isService = source.isService; + this.mapping = copies.copy(source.mapping); + this.service = copies.copy(source.service); + } + + /** Copies a completed query, with fresh containers and no retained planning producer. */ + @Override + public Query copyPlan(PlanCopy copies) { + if (!isCompiled) { + throw new IllegalStateException("Only completed queries can form an execution image"); + } + return new Query(this, copies); } Query(Exp e) { @@ -215,8 +269,8 @@ public static Query create(Exp e) { return new Query(e); } - @SuppressWarnings("java:S1172") // Parameter preserved for legacy factory compatibility - public static Query create(int type) { + /** Creates an empty compiled query. */ + public static Query create() { return new Query(); } @@ -277,12 +331,12 @@ public List getQueries() { @Override public Object getObject() { - return object; + return queryObject; } @Override public void setObject(Object o) { - object = o; + queryObject = o; } public int getPlanProfile() { @@ -343,7 +397,7 @@ public void setBody(Exp exp){ } void setGlobalQuery(Query q) { - query = q; + globalQuery = q; inherit(q); } @@ -360,8 +414,8 @@ void inherit(Query q) { public Query getGlobalQuery() { - if (query != null) { - return query; + if (globalQuery != null) { + return globalQuery; } return this; } @@ -378,7 +432,7 @@ public Query getOuterQuery() { } public boolean isSubQuery() { - return query != null; + return globalQuery != null; } @@ -391,11 +445,6 @@ public boolean isSelectExpression(){ return false; } - public boolean isCheckLoop() { - return isCheckLoop; - } - - /** * Fake local graph node */ @@ -408,17 +457,6 @@ public void setGraphNode(Node n) { gNode = n; } - public Node getPathNode() { - return pathNode; - } - - /** - * constraint in property path: ?x ex:prop @[?this != ] ?y - */ - List getPathFilter() { - return pathFilter; - } - public List getFunList(){ return funList; } @@ -592,11 +630,11 @@ public int getOffset() { } public boolean isDistinct() { - return distinct; + return distinctResults; } public void setDistinct(boolean b) { - distinct = b; + distinctResults = b; } public boolean isCheck() { @@ -605,12 +643,12 @@ public boolean isCheck() { @Override public boolean isAggregate() { - return isAggregate; + return aggregateQuery; } @Override public void setAggregate(boolean b) { - isAggregate = b; + aggregateQuery = b; } public boolean isRelax() { @@ -622,8 +660,8 @@ public void setRelax(boolean b) { } public boolean isDistribute() { - if (query != null) { - return query.isDistribute(); + if (globalQuery != null) { + return globalQuery.isDistribute(); } else { return isDistribute; } @@ -752,14 +790,6 @@ public boolean isListGroup() { return isListGroup; } - public boolean isListPath() { - return isListPath; - } - - public boolean isCountPath() { - return isCountPath; - } - public boolean isCorrect() { return isCorrect; } @@ -894,9 +924,6 @@ void basicComplete(Producer prod) { * index(node) use global query index */ void complete2() { - for (Filter f : getPathFilter()) { - index(this, f); - } index(getOrderBy()); index(getGroupBy()); @@ -919,14 +946,11 @@ void complete2() { if (getGraphNode() != null) { index(getGraphNode()); } - if (getPathNode() != null) { - index(getPathNode()); - } } void index(List list) { for (Exp ee : list) { - // use case: group by (exists(?x :p ?y) as ?b) + // use case: group by (exists(?x :p ?y) as ?b) // use case: order by exists(?x :p ?y) if (ee.getFilter() != null) { index(this, ee.getFilter()); @@ -1017,15 +1041,6 @@ void store(Node node, boolean exist, boolean select) { * called by compiler transformer */ public void collect() { - if (getPathNode() != null) { - /* - * use case: ?x ex:prop @[?this != ] + ?y collect ?this first - * because it may be within @[exists {?this ?p ?y}} and even worse - * within @[exists {select ?this where {?this ?p ?y}}] - */ - store(getPathNode(), false, false); - } - for (Exp ee : this) { collect(ee, false); } @@ -1038,10 +1053,6 @@ public void collect() { store(node, false, false); } - for (Filter ff : getPathFilter()) { - collectExist(ff.getExp()); - } - for (Filter f : getFunList()){ collectExist(f.getExp()); } @@ -1049,13 +1060,10 @@ public void collect() { // exist: inside exists ( exp ) // or inside A minus ( exp ) - @SuppressWarnings("java:S3776") void collect(Exp exp, boolean exist) { switch (exp.type()) { case FILTER: - // get exists () nodes - // draft collectExist(exp.getFilter().getExp()); break; @@ -1069,16 +1077,8 @@ void collect(Exp exp, boolean exist) { } break; - case EDGE, PATH: - Edge edge = exp.getEdge(); - store(edge.getNode(0), exist, false); - if (edge.getEdgeVariable() != null) { - store(edge.getEdgeVariable(), exist, false); - } - store(edge.getNode(1), exist, false); - for (int i = 2; i < edge.nbNode(); i++) { - store(edge.getNode(i), exist, false); - } + case EDGE: + collectEdgeNodes(exp.getEdge(), exist); break; case XPATH, EVAL: @@ -1089,13 +1089,7 @@ void collect(Exp exp, boolean exist) { break; case MINUS: - // second argument does not bind anything: skip it - if (exp.first() != null) { - collect(exp.first(), exist); - } - if (exp.rest() != null) { - collect(exp.rest(), true); - } + collectMinusNodes(exp, exist); break; case QUERY: @@ -1105,14 +1099,7 @@ void collect(Exp exp, boolean exist) { break; case BIND: - collectExist(exp.getFilter().getExp()); - store(exp.getNode(), exist, false); //true); - if (exp.getNodeList() != null){ - // values {unnest()} compiled as bind () - for (Node n : exp.getNodeList()){ - store(n, exist, false); //true); - } - } + collectBindNodes(exp, exist); break; default: @@ -1123,6 +1110,37 @@ void collect(Exp exp, boolean exist) { } + private void collectEdgeNodes(Edge edge, boolean insideExistencePattern) { + store(edge.getNode(0), insideExistencePattern, false); + if (edge.getEdgeVariable() != null) { + store(edge.getEdgeVariable(), insideExistencePattern, false); + } + store(edge.getNode(1), insideExistencePattern, false); + for (int index = 2; index < edge.nbNode(); index++) { + store(edge.getNode(index), insideExistencePattern, false); + } + } + + private void collectMinusNodes(Exp expression, boolean insideExistencePattern) { + if (expression.first() != null) { + collect(expression.first(), insideExistencePattern); + } + // The right operand is indexed but does not introduce solution bindings. + if (expression.rest() != null) { + collect(expression.rest(), true); + } + } + + private void collectBindNodes(Exp expression, boolean insideExistencePattern) { + collectExist(expression.getFilter().getExp()); + store(expression.getNode(), insideExistencePattern, false); + if (expression.getNodeList() != null) { + for (Node node : expression.getNodeList()) { + store(node, insideExistencePattern, false); + } + } + } + void collectExist(Expr exp) { if (exp.oper() == ExprType.EXIST) { Exp pat = getPattern(exp); @@ -1143,9 +1161,9 @@ void collectExist(Expr exp) { */ int index(Query query, Exp exp, boolean isExist, int start) { int min = switch (exp.type()) { - case EDGE, PATH, XPATH, EVAL -> indexEdgeExp(query, exp); - case VALUES -> indexValuesExp(query, exp); - case NODE -> qIndex(query, exp.getNode()); + case EDGE, XPATH, EVAL -> indexEdgeExp(exp); + case VALUES -> indexValuesExp(exp); + case NODE -> index(exp.getNode()); case BIND -> indexBindExp(query, exp, isExist); case FILTER -> indexExpFilter(query, exp, isExist); case QUERY -> indexExpQuery(query, exp, isExist); @@ -1160,22 +1178,16 @@ int index(Query query, Exp exp, boolean isExist, int start) { return min; } - private int indexEdgeExp(Query query, Exp exp) { + private int indexEdgeExp(Exp exp) { Edge edge = exp.getEdge(); edge.setEdgeIndex(iEdge++); - int min = indexExpEdge(query, exp); - if (exp.hasPath()) { - Exp ep = exp.getPath(); - ep.getEdge().setEdgeIndex(edge.getEdgeIndex()); - indexExpEdge(query, ep); - } - return min; + return indexExpEdge(exp); } - private int indexValuesExp(Query query, Exp exp) { + private int indexValuesExp(Exp exp) { int min = Integer.MAX_VALUE; for (Node node : exp.getNodeList()) { - int n = qIndex(query, node); + int n = index(node); min = Math.min(min, n); } return min; @@ -1183,10 +1195,10 @@ private int indexValuesExp(Query query, Exp exp) { private int indexBindExp(Query query, Exp exp, boolean isExist) { Node qn = exp.getNode(); - int min = qIndex(query, qn); + int min = index(qn); if (exp.getNodeList() != null) { for (Node bn : exp.getNodeList()) { - int ii = qIndex(query, bn); + int ii = index(bn); min = Math.min(min, ii); } } @@ -1218,7 +1230,7 @@ int indexExpFilter(Query query, Exp exp, boolean isExist) { logger.warn(Message.Prefix.UNDEF_VAR.getString(), variable); addError(Message.Prefix.UNDEF_VAR.getString(), variable); } else if (!isExist && !hasExist) { - int n = qIndex(query, qNode); + int n = index(qNode); min = Math.min(min, n); } } @@ -1247,7 +1259,7 @@ int indexExpQuery(Query query, Exp exp, boolean isExist) { // use case: query node created to hold fun result // see Transformer compiler.compileSelectFun() Node sNode = ee.getNode(); - int n = qIndex(qq, sNode); + int n = index(sNode); min = Math.min(min, n); if (ee.getFilter() != null) { @@ -1259,35 +1271,22 @@ int indexExpQuery(Query query, Exp exp, boolean isExist) { return min; } - int indexExpEdge(Query query, Exp exp) { + private int indexExpEdge(Exp exp) { int min = Integer.MAX_VALUE; for (int i = 0; i < exp.nbNode(); i++) { - int n = qIndex(query, exp.getNode(i)); + int n = index(exp.getNode(i)); min = Math.min(min, n); } return min; } - @SuppressWarnings("java:S3400") // Placeholder method for future RDF-star variable annotations - boolean isTriple(Exp exp, String name) { - // RDF-star variable annotations belonged to the legacy parser expression. - // The native AST represents triple terms explicitly, so a plain variable - // reference cannot carry that hidden flag. - return false; - } - - /** - * Generate or retrieve index of node - * If node is in a sub query, return the - * index of the outer node corresponding to node and rec. - */ - @SuppressWarnings("java:S1172") // Query parameter preserved for outer query scoping - int qIndex(Query query, Node node) { - return index(node); - } - /** + * Returns a node's existing index or assigns one from the outer query's + * binding space. Subqueries use the same index allocator as their parent. + * + * @param node node to index + * @return existing or newly assigned binding index */ public int index(Node node) { if (node.getIndex() == -1) { @@ -1319,6 +1318,7 @@ int getNodeIndex() { * * @return list Exp(node) for select * node list */ + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. public List toExp(List lNode) { List lExp = new ArrayList<>(); for (Node node : lNode) { @@ -1462,46 +1462,6 @@ public boolean isDetail() { return isDetail; } - public void setSynchronized(boolean b) { - isSynchronized = b; - } - - public boolean isSynchronized() { - return isSynchronized; - } - - public void setTransformer(String p, Object transformer) { - getGlobalQuery().setPPrinter(p, transformer); - } - - Object getPPrinter(String p) { - if (p == null) { - return transformer; - } - return tprinter.get(p); - } - - void setPPrinter(String p, Object transformer) { - if (p == null) { - // next kg:pprint() will use this one - this.transformer = transformer; - } else { - if (this.transformer == null) { - // next kg:pprint() will use this one - this.transformer = transformer; - } - tprinter.putIfAbsent(p, transformer); - } - } - - public void setTemplate(boolean template) { - isTemplate = template; - } - - public boolean isTemplate() { - return isTemplate; - } - @Override public int getNumber() { return number; @@ -1513,34 +1473,12 @@ public void setNumber(int number) { } - public Exp getTemplateGroup() { - return templateGroup; - } - public int getQueryProfile() { return queryProfile; } - public boolean isStorePath() { - return isStorePath; - } - - - public boolean isCachePath() { - return isCachePath; - } - - public void setExtension(boolean b) { - isExtension = b; - } - - public boolean isExtension() { - return isExtension; - } - - @Override public Iterable getLoop() { return () -> getEdges().stream().map(Object.class::cast).iterator(); @@ -1563,14 +1501,6 @@ public boolean isUseBind() { } - public BgpGenerator getBgpGenerator() { - return bgpGenerator; - } - - public void setBgpGenerator(BgpGenerator bgpGenerator) { - this.bgpGenerator = bgpGenerator; - } - public boolean isReportEnabled() { return getGlobalQuery().reportEnabled; @@ -1581,10 +1511,6 @@ public void setReportEnabled(boolean reportEnabled) { } - public boolean isTransformationTemplate() { - return isTransformationTemplate; - } - @Override @@ -1601,26 +1527,6 @@ public boolean isInsert() { return isInsert; } - public boolean isLock() { - return lock; - } - - - public void setLock(boolean lock) { - this.lock = lock; - } - - - public boolean isParallel() { - return parallel; - } - - - public void setParallel(boolean parallel) { - this.parallel = parallel; - } - - public boolean isFederate() { return federate; } @@ -1641,13 +1547,5 @@ public void setValidate(boolean validate) { } - public boolean isAlgebra() { - return algebra; - } - - - public void setAlgebra(boolean algebra) { - this.algebra = algebra; - } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/BasicPatternGenerator.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/BindingPatternOrder.java similarity index 67% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/BasicPatternGenerator.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/BindingPatternOrder.java index 71d0e33ca..add993bdc 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/BasicPatternGenerator.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/BindingPatternOrder.java @@ -1,18 +1,18 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.BOUND; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.NA; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.OBJECT; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.PREDICATE; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.SUBJECT; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.UNBOUND; +package fr.inria.corese.core.next.query.impl.engine.planning; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.BOUND; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.NA; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.OBJECT; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.PREDICATE; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.SUBJECT; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.UNBOUND; /** - * Generate the basic patterns ordering by the selectivity acorrding to the size - * of Subject, Predicate and Object Ns, Np and No + * Ranks subject, predicate, and object binding patterns using distinct-term counts. + * Falls back to the default predicate/subject/object order when counts are unavailable. * * @author Fuqi Song, Wimmics Inria I3S */ -public final class BasicPatternGenerator { +public final class BindingPatternOrder { private static final int P = 1; private static final int S = 0; @@ -21,11 +21,11 @@ public final class BasicPatternGenerator { private static final int TRIPLE_LENGTH = 3; private static final int PATTERN_LENGTH = 8; - private BasicPatternGenerator() { + private BindingPatternOrder() { } - private static int[] getNumbers(IProducerQP ip) { + private static int[] getNumbers(QueryStatistics ip) { int[] numbers = null; if (ip != null && ip.getSize(SUBJECT) != NA @@ -41,11 +41,12 @@ private static int[] getNumbers(IProducerQP ip) { } /** - * Generate the basic patterns using the numbers of distince subject, - * predicate and objects + * Generates the eight binding patterns in estimated selectivity order. * + * @param producer distinct-term statistics, or {@code null} for the default order + * @return ordered subject/predicate/object binding patterns */ - public static int[][] generateBasicPattern(IProducerQP producer) { + public static int[][] generateBasicPattern(QueryStatistics producer) { //1 step: get the order of s, p, o according to the number of distinct s, p, o // using default settings if the numbers of s, p, o are not available int[] order = DEFAULT_ORDER; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/CardinalityPatternOrdering.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/CardinalityPatternOrdering.java new file mode 100644 index 000000000..1f8c373c4 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/CardinalityPatternOrdering.java @@ -0,0 +1,160 @@ +package fr.inria.corese.core.next.query.impl.engine.planning; + +import fr.inria.corese.core.next.query.impl.engine.model.Edge; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.path.PropertyPathEdge; +import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; + +import java.util.ArrayList; +import java.util.List; + +/** + * Stable greedy ordering of contiguous ordinary triple patterns. Every other + * expression is a semantic barrier, including BIND, VALUES, paths, and graph + * operators. Only planning allocates scratch space; statement traversal is untouched. + */ +final class CardinalityPatternOrdering { + + private CardinalityPatternOrdering() { + } + + /** + * Chooses the smallest known pattern first, then favors connected joins. + * Unknown or equal counts fall back to binding connectivity and source order. + * Each pattern is counted once, never from a sorting comparator. + * + * @param query query supplying the active dataset + * @param graph enclosing graph, or {@code null} for the default graph + * @param expression expression group to order in place + * @param variables variables available from the enclosing scope + * @param bindings constant-binding hints collected by the planner + * @param statistics producer's session-visible statistics + */ + static void sort(Query query, Node graph, Exp expression, List variables, + List bindings, QueryStatistics statistics) { + flattenTripleGroups(expression); + List bound = new ArrayList<>(); + int start = 0; + while (start < expression.size()) { + if (!isTriple(expression.get(start))) { + recordBindings(expression.get(start++), bound); + continue; + } + int end = start + 1; + while (end < expression.size() && isTriple(expression.get(end))) { + end++; + } + if (end - start > 1) { + long[] counts = cardinalities(query, graph, expression, start, end, statistics); + orderRun(expression, start, end, counts, bound, variables, bindings); + } else { + recordBindings(expression.get(start), bound); + } + start = end; + } + } + + // The parser may emit one BGP per dotted triple. Adjacent pure triple groups + // are associative joins; explicit groups remain JOIN operators and are retained. + private static void flattenTripleGroups(Exp expression) { + int i = 0; + while (i < expression.size()) { + Exp child = expression.get(i); + if (isTripleGroup(child)) { + expression.remove(child); + for (int j = 0; j < child.size(); j++) { + expression.add(i + j, child.get(j)); + } + i += child.size(); + } else { + i++; + } + } + } + + private static boolean isTripleGroup(Exp expression) { + if (!expression.isBGP() || expression.size() == 0) { + return false; + } + for (Exp child : expression) { + if (!isTriple(child)) { + return false; + } + } + return true; + } + + private static boolean isTriple(Exp expression) { + return expression.isEdge() && !(expression.getEdge() instanceof PropertyPathEdge); + } + + private static long[] cardinalities(Query query, Node graph, Exp expression, int start, int end, + QueryStatistics statistics) { + long[] counts = new long[end - start]; + for (int i = start; i < end; i++) { + counts[i - start] = statistics.estimateCardinality(query, graph, expression.get(i).getEdge()); + } + return counts; + } + + private static void orderRun(Exp expression, int start, int end, long[] counts, + List bound, List variables, List bindings) { + for (int i = start; i < end; i++) { + int best = i; + for (int j = i + 1; j < end; j++) { + if (before(expression.get(j), expression.get(best), counts[j - start], counts[best - start], + bound, variables, bindings)) { + best = j; + } + } + moveFirst(expression, counts, start, i, best); + recordBindings(expression.get(i), bound); + } + } + + private static void recordBindings(Exp expression, List bound) { + expression.bind(bound); + if (expression.isEdge()) { + Node predicate = expression.getEdge().getEdgeVariable(); + if (predicate != null && !bound.contains(predicate)) { + bound.add(predicate); + } + } + } + + private static boolean before(Exp candidate, Exp current, long candidateCount, long currentCount, + List bound, List variables, List bindings) { + boolean candidateConnected = connected(candidate.getEdge(), bound, variables); + boolean currentConnected = connected(current.getEdge(), bound, variables); + if (candidateConnected != currentConnected) { + return candidateConnected; + } + if (candidateCount >= 0 && currentCount >= 0 && candidateCount != currentCount) { + return candidateCount < currentCount; + } + return candidate.nBind(bound, variables, bindings) > current.nBind(bound, variables, bindings); + } + + private static boolean connected(Edge edge, List bound, List variables) { + return bound(edge.getNode(0), bound, variables) + || bound(edge.getNode(1), bound, variables) + || bound(edge.getEdgeVariable(), bound, variables); + } + + private static boolean bound(Node node, List bound, List variables) { + return node != null && node.isVariable() + && (bound.contains(node) || variables.contains(node.getLabel())); + } + + private static void moveFirst(Exp expression, long[] counts, int start, int position, int best) { + Exp selected = expression.get(best); + long count = counts[best - start]; + for (int i = best; i > position; i--) { + expression.set(i, expression.get(i - 1)); + counts[i - start] = counts[i - start - 1]; + } + expression.set(position, selected); + counts[position - start] = count; + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/Sorter.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/ConnectedPatternOrdering.java similarity index 86% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/Sorter.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/ConnectedPatternOrdering.java index c78b751c1..6c621eaa3 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/Sorter.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/ConnectedPatternOrdering.java @@ -1,4 +1,4 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; import fr.inria.corese.core.next.query.impl.engine.pattern.Query; import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; @@ -10,12 +10,12 @@ import java.util.List; /** - * Sort KGRAM edges in connected order before query process + * Orders triple patterns so successive patterns share bound variables where possible. * * @author Olivier Corby, Edelweiss, INRIA 2011 * */ -public class Sorter { +public class ConnectedPatternOrdering { @SuppressWarnings("java:S3776") @@ -53,6 +53,12 @@ public void sort(Query q, Exp exp, List lVar, List lBind) { } + /** + * Keeps the source seed for the connectivity-only fallback. Statistical + * ordering chooses its seed independently through {@link HeuristicPatternOrdering}. + * + * @return whether this fallback preserves the first unbound pattern + */ public boolean leaveFirst() { return true; } @@ -62,13 +68,6 @@ public boolean leaveFirst() { * count 0 */ boolean shift(Query q, Exp e1, Exp e2, List lNode, List lVar, List lBind) { - if (e1.isGraphPath(e2, e1)) { - return true; - } - if (e1.isGraphPath(e1, e2)) { - return false; - } - return before(q, e1, e2, lNode, lVar, lBind); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/DepthFirstBestSearch.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/GreedyPatternOrdering.java similarity index 65% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/DepthFirstBestSearch.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/GreedyPatternOrdering.java index 6715d8948..ea8c49f14 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/DepthFirstBestSearch.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/GreedyPatternOrdering.java @@ -1,4 +1,4 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; @@ -10,49 +10,45 @@ import static fr.inria.corese.core.next.query.impl.engine.model.ExpType.Type; /** - * An implementation for sorting the triple pattern depth-first & best greedy - * algorithm - * 1 start with the one with least cost, if there are more than 2 with same cost - * then choose the one with more edges, if still the same, then just pick one - * 2 search next node by considering all the adjacent nodes of the visited nodes - * 3 recursively search until all nodes have been visited - * 4 every time one node is added to visited, check FILTER & VALUES - * ++ 4.1 now also check BIND 20Jan2015 + * Orders a pattern graph using a greedy depth-first traversal. The least-cost + * node starts each connected component, with connectivity used to break ties. + * FILTER, VALUES, and BIND dependencies are considered as nodes become available. + * Instances hold traversal state and are confined to one planning operation. * * @author Fuqi Song, Wimmics Inria I3S */ -public class DepthFirstBestSearch implements ISort { +public class GreedyPatternOrdering implements PatternOrderingStrategy { - private final List visited; - private final List notVisited; - private final List binds; - private QPGraph g = null; + private final List visited; + private final List notVisited; + private final List binds; + private QueryPlanGraph g = null; - public DepthFirstBestSearch() { + public GreedyPatternOrdering() { this.visited = new ArrayList<>(); this.notVisited = new ArrayList<>(); this.binds = new ArrayList<>(); } @Override - public List sort(QPGraph graph) { + public List sort(QueryPlanGraph graph) { this.g = graph; //EDGE GRAPH - List sortableNodes = g.getAllNodes(Type.EDGE); + List sortableNodes = g.getAllNodes(Type.EDGE); sortableNodes.addAll(g.getAllNodes(Type.GRAPH)); notVisited.addAll(sortableNodes); binds.addAll(g.getAllNodes(Type.BIND)); //each loop is a sub graph while (!notVisited.isEmpty()) { - Map nextNodesPool = new LinkedHashMap<>(); - QPGNode first = findFirst(notVisited, nextNodesPool); + Map nextNodesPool = new LinkedHashMap<>(); + QueryPlanNode first = findFirst(notVisited, nextNodesPool); route(first, nextNodesPool); } //for other left nodes, ex. filter/values/bind not related to any vairables, optional //just add them at the end of visited list - for (QPGNode n : g.getAllNodes()) { + for (QueryPlanNode n : g.getAllNodes()) { if (!visited.contains(n)) { visited.add(n); } @@ -61,24 +57,24 @@ public List sort(QPGraph graph) { } //search in one connected graph, depth-first - private void route(QPGNode previous, Map nextNodesPool) { + private void route(QueryPlanNode previous, Map nextNodesPool) { if (previous == null) return; // while there is still node not visited in the pool - QPGNode next; + QueryPlanNode next; while ((next = findNext(nextNodesPool)) != null) { route(next, nextNodesPool); } } //Find the bpg node with the smallest selectivity as starting routing point - private QPGNode findFirst(List lNodes, Map pool) { + private QueryPlanNode findFirst(List lNodes, Map pool) { if (lNodes == null || lNodes.isEmpty()) return null; - QPGNode minNode = lNodes.getFirst(); - for (QPGNode node : lNodes) { + QueryPlanNode minNode = lNodes.getFirst(); + for (QueryPlanNode node : lNodes) { double cost = node.getCost(); - if (cost == IEstimate.NA_COST) continue; + if (cost == SelectivityEstimator.NA_COST) continue; //number of linked edges int es1 = this.g.getEdges(node) == null ? 0 : this.g.getEdges(node).size(); int es2 = this.g.getEdges(minNode) == null ? 0 : this.g.getEdges(minNode).size(); @@ -94,15 +90,15 @@ private QPGNode findFirst(List lNodes, Map pool) { } //find next node to be routed - private QPGNode findNext(Map pool) { + private QueryPlanNode findNext(Map pool) { // 1. if no nodes left, return null if (pool.isEmpty()) return null; // 2. find the node with smallest cost - QPGNode next = null; + QueryPlanNode next = null; double cost = Double.MAX_VALUE; - for (Map.Entry entry : pool.entrySet()) { - QPGNode qPGNode = entry.getKey(); + for (Map.Entry entry : pool.entrySet()) { + QueryPlanNode qPGNode = entry.getKey(); Double double1 = entry.getValue(); if (double1 < cost) { cost = double1; @@ -116,7 +112,7 @@ private QPGNode findNext(Map pool) { return next; } - private void queue(QPGNode minNode, Map pool) { + private void queue(QueryPlanNode minNode, Map pool) { //1.0 check VALUES, if linked to this node, put it before this.addValues(minNode); @@ -138,9 +134,9 @@ private void queue(QPGNode minNode, Map pool) { //for a filter, if all linked vairables have been visited, //then add this filter just after these triple patterns private void addFilters() { - List filters = this.g.getAllNodes(Type.FILTER); + List filters = this.g.getAllNodes(Type.FILTER); - for (QPGNode f : filters) { + for (QueryPlanNode f : filters) { //if there are no edges left linking to the filter (values, etc...) //add this filter to the sequence, just after the triples patterns if (!visited.contains(f) && isDisjoint(this.g.getLinkedNodes(f), notVisited, binds)) { @@ -150,10 +146,10 @@ private void addFilters() { } //for VALUES, put it just before the vairables being used - private void addValues(QPGNode min) { - List values = this.g.getAllNodes(Type.VALUES); + private void addValues(QueryPlanNode min) { + List values = this.g.getAllNodes(Type.VALUES); - for (QPGNode v : values) { + for (QueryPlanNode v : values) { //if the VALUES use this (first) triple pattern, then add it to //visited list, before all linked nodes if (!visited.contains(v) && g.getLinkedNodes(v).contains(min)) { @@ -163,20 +159,20 @@ private void addValues(QPGNode min) { } //Find the adjacent nodes linked to a given node and add them to a pool - private void findAdjacentNodes(QPGNode minNode, Map pool) { + private void findAdjacentNodes(QueryPlanNode minNode, Map pool) { //add adjacent unvisted nodes to a pool - List lEdges = this.g.getEdges(minNode, QPGEdge.BI_DIRECT); + List lEdges = this.g.getEdges(minNode, QueryPlanEdge.BI_DIRECT); - for (QPGEdge e : lEdges) { + for (QueryPlanEdge e : lEdges) { // get the linked node - QPGNode linkedNode = e.get(minNode); + QueryPlanNode linkedNode = e.get(minNode); //get the cost of the linked node double cost = e.getCost() * linkedNode.getCost(); //check the cost between the linked node and existing visited node - for (QPGNode vn : this.visited) { + for (QueryPlanNode vn : this.visited) { //if there is edge between vn and node - QPGEdge linkedEdge = this.g.getEdge(minNode, vn); + QueryPlanEdge linkedEdge = this.g.getEdge(minNode, vn); if (linkedEdge != null) { double cost2 = linkedEdge.getCost() * vn.getCost(); if (cost2 < cost) { @@ -192,9 +188,9 @@ private void findAdjacentNodes(QPGNode minNode, Map pool) { } //Check BIND expressions - private void addBinds(QPGNode min) { - for (QPGNode q : g.getLinkedNodes(min)) { - List linkedBinds = g.getLinkedNodes(q, true, true); + private void addBinds(QueryPlanNode min) { + for (QueryPlanNode q : g.getLinkedNodes(min)) { + List linkedBinds = g.getLinkedNodes(q, true, true); //for BIND, it also can have FILTERs and VALUES, and also it can depends on the //other BINDs expressions, so need to check recusively @@ -213,8 +209,8 @@ private void addBinds(QPGNode min) { /** * Check if the first list has same nodes with the other two lists of nodes */ - private boolean isDisjoint(List list, List notVisited, List binds) { - for (QPGNode q : list) { + private boolean isDisjoint(List list, List notVisited, List binds) { + for (QueryPlanNode q : list) { if (notVisited.contains(q) || binds.contains(q)) { return true; } @@ -224,9 +220,9 @@ private boolean isDisjoint(List list, List notVisited, List nodes, int start) { + public void rewrite(Exp exp, List nodes, int start) { for (int i = 0; i < nodes.size(); i++) { - QPGNode node = nodes.get(i); + QueryPlanNode node = nodes.get(i); exp.set(start + i, node.getExp()); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/SorterNew.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/HeuristicPatternOrdering.java similarity index 60% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/SorterNew.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/HeuristicPatternOrdering.java index 76b1cabf5..86557b073 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/SorterNew.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/HeuristicPatternOrdering.java @@ -1,6 +1,8 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; +import fr.inria.corese.core.next.query.impl.engine.model.Node; import fr.inria.corese.core.next.query.impl.engine.model.ExpType; import fr.inria.corese.core.next.query.impl.engine.spi.Producer; @@ -11,13 +13,29 @@ import java.util.Map; import java.util.Map.Entry; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.plannable; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.plannable; /** - * A new sorter for QP - * + * Reorders each plannable expression group using selectivity estimates and + * shared-variable connections. Semantic barriers retain their source positions. */ -public class SorterNew extends Sorter { +public class HeuristicPatternOrdering extends ConnectedPatternOrdering { + + /** + * Orders ordinary triple runs using native cardinalities and stable connectivity. + * Non-triple expressions retain their positions; the planner places filters afterward. + * + * @param query query supplying the active dataset + * @param graph enclosing GRAPH node, or {@code null} + * @param expression expression group to rewrite + * @param variables variables available from the enclosing scope + * @param bindings constant-binding hints + * @param statistics producer's statistics capability + */ + public void sort(Query query, Node graph, Exp expression, List variables, + List bindings, QueryStatistics statistics) { + CardinalityPatternOrdering.sort(query, graph, expression, variables, bindings, statistics); + } @SuppressWarnings("java:S3776") public void sort(Exp expression, List bindings, Producer prod) { @@ -33,28 +51,28 @@ public void sort(Exp expression, List bindings, Producer prod) { List exps = entrySet.getValue(); // ** 1 create graph (list of nodes and their edges) ** - QPGraph bpg = new QPGraph(exps, bindings); + QueryPlanGraph bpg = new QueryPlanGraph(exps, bindings); // ** 2 estimate cost/selectivity ** // ** 2.1 find the corresponding algorithm ** - IEstimate ies = new HeuristicsBasedEstimation(); + SelectivityEstimator ies = new HeuristicSelectivityEstimator(); // ** 2.2 estimate ** - ies.estimate(bpg, prod, bindings); + ies.estimate(bpg, prod); // ** 3 sort and find the order ** - ISort is; - is = new DepthFirstBestSearch(); + PatternOrderingStrategy is; + is = new GreedyPatternOrdering(); - List l = is.sort(bpg); + List l = is.sort(bpg); // For the case where a graph has a bound value, put the BIND before // the graph pattern (because normally we put BIND just after where its variable is used). // Example: when binding a graph variable before a graph pattern - List graphs = bpg.getAllNodes(ExpType.Type.GRAPH); - for (QPGNode graph : graphs) { - List linkedNodes = bpg.getLinkedNodes(graph); - for (QPGNode bind : linkedNodes) { + List graphs = bpg.getAllNodes(ExpType.Type.GRAPH); + for (QueryPlanNode graph : graphs) { + List linkedNodes = bpg.getLinkedNodes(graph); + for (QueryPlanNode bind : linkedNodes) { if (bind.getType() == ExpType.Type.BIND) { int iBind = l.indexOf(bind); int iGraph = l.indexOf(graph); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/HeuristicsBasedEstimation.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/HeuristicSelectivityEstimator.java similarity index 52% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/HeuristicsBasedEstimation.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/HeuristicSelectivityEstimator.java index eaf9eaf2e..a16b82b7f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/HeuristicsBasedEstimation.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/HeuristicSelectivityEstimator.java @@ -1,4 +1,4 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; import fr.inria.corese.core.next.query.impl.engine.model.ExpType; import fr.inria.corese.core.next.query.impl.engine.spi.Producer; @@ -7,19 +7,19 @@ import java.util.List; /** - * Estimate the selectivity of triple pattern by heuristics + * Estimates pattern and join selectivity using bindings, filters, and connectivity. * * @author Fuqi Song, Wimmics Inria I3S */ -public class HeuristicsBasedEstimation implements IEstimate { +public class HeuristicSelectivityEstimator implements SelectivityEstimator { - private QPGraph graph; - private IProducerQP producer; + private QueryPlanGraph graph; + private QueryStatistics producer; @Override - public void estimate(QPGraph graph, Producer producer, Object parameters) { + public void estimate(QueryPlanGraph graph, Producer producer) { this.graph = graph; - this.producer = producer instanceof IProducerQP queryProducer ? queryProducer : null; + this.producer = producer instanceof QueryStatistics queryProducer ? queryProducer : null; estimateNodes(); estimateEdges(); @@ -28,10 +28,10 @@ public void estimate(QPGraph graph, Producer producer, Object parameters) { //assign costs for nodes based on ?-tuple pattern private void estimateNodes() { // 1. get all models of nodes in QPG - List models = new ArrayList<>(); - for (QPGNode n : this.graph.getAllNodes()) { + List models = new ArrayList<>(); + for (QueryPlanNode n : this.graph.getAllNodes()) { if (n.getType() == ExpType.Type.EDGE || n.getType() == ExpType.Type.GRAPH) { - QPGNodeCostModel p = n.getCostModel(); + NodeCostModel p = n.getCostModel(); if (n.getType() == ExpType.Type.EDGE) { p.setParameters(graph); } @@ -41,8 +41,8 @@ private void estimateNodes() { //** 3 sort by priority //generate basic patterns using numbers({Ns Np No} | null) - int[][] basicPatterns = BasicPatternGenerator.generateBasicPattern(producer); - QPGNodeCostModel.sort(models, basicPatterns, producer); + int[][] basicPatterns = BindingPatternOrder.generateBasicPattern(producer); + NodeCostModel.sort(models, basicPatterns, producer); //--put the same triple patterns in one list and assign the same selectivity to //--all the patterns in this list, needs to be improved by distinguwish the same patterns @@ -51,25 +51,25 @@ private void estimateNodes() { // l2: t21 // l3: t31, t32, t33 // l4.. - List> modelList = groupModels(models, basicPatterns); + List> modelList = groupModels(models, basicPatterns); //** 2 assign cost for (int i = 0; i < modelList.size(); i++) { - for (AbstractCostModel model : modelList.get(i)) { - model.estimate(List.of(modelList.size(), i)); + for (NodeCostModel model : modelList.get(i)) { + model.estimate(modelList.size(), i); } } } - private List> groupModels(List models, int[][] patterns) { - List> groups = new ArrayList<>(); + private List> groupModels(List models, int[][] patterns) { + List> groups = new ArrayList<>(); int index = 0; while (index < models.size()) { - QPGNodeCostModel first = models.get(index++); - List group = new ArrayList<>(); + NodeCostModel first = models.get(index++); + List group = new ArrayList<>(); group.add(first); while (index < models.size() - && QPGNodeCostModel.compareModel(first, models.get(index), patterns, producer) == 0) { + && NodeCostModel.compareModel(first, models.get(index), patterns, producer) == 0) { group.add(models.get(index++)); } groups.add(group); @@ -79,8 +79,8 @@ private List> groupModels(List models, //assign weight/sel for edge between triple pattern private void estimateEdges() { - for (QPGEdge edge : graph.getEdges(QPGEdge.BI_DIRECT)) { - edge.getCostModel().estimate(null); + for (QueryPlanEdge edge : graph.getEdges(QueryPlanEdge.BI_DIRECT)) { + edge.getCostModel().estimate(); } } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGEdgeCostModel.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/JoinCostModel.java similarity index 57% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGEdgeCostModel.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/JoinCostModel.java index ac58c7557..58302a945 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGEdgeCostModel.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/JoinCostModel.java @@ -1,39 +1,38 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; import fr.inria.corese.core.next.query.impl.engine.model.ExpType; -import java.util.List; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.OBJECT; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.PREDICATE; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.SUBJECT; -import static fr.inria.corese.core.next.query.impl.engine.sorter.IEstimate.MAX_COST; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.OBJECT; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.PREDICATE; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.SUBJECT; +import static fr.inria.corese.core.next.query.impl.engine.planning.SelectivityEstimator.MAX_COST; /** - * Cost model for QPG edge + * Estimates join cost from endpoint binding patterns and shared variables. * * @author Fuqi Song, Wimmics Inria I3S */ -public class QPGEdgeCostModel extends AbstractCostModel { +public final class JoinCostModel { //weight 6, 5, 4, 3, 2, 1 private static final int[][] JOINT_PATTERN = new int[][]{ {PREDICATE, OBJECT}, {SUBJECT, PREDICATE}, {SUBJECT, OBJECT}, {OBJECT, OBJECT}, {SUBJECT, SUBJECT}, {PREDICATE, PREDICATE} }; - private final QPGEdge edge; + private final QueryPlanEdge edge; private int nShare = 0; private int jType = -1; - public QPGEdgeCostModel(QPGEdge edge) { + public JoinCostModel(QueryPlanEdge edge) { this.edge = edge; if (this.estimatable()) { if (this.edge.get(0).getType() == ExpType.Type.EDGE && this.edge.get(1).getType() == ExpType.Type.EDGE) { this.setJType(); } this.setNShare(); - this.edge.setType(QPGEdge.BI_DIRECT); + this.edge.setType(QueryPlanEdge.BI_DIRECT); } else { - this.edge.setType(QPGEdge.SIMPLE); + this.edge.setType(QueryPlanEdge.SIMPLE); } } @@ -42,8 +41,8 @@ private void setNShare() { } private void setJType() { - QPGNode node1 = edge.get(0); - QPGNode node2 = edge.get(1); + QueryPlanNode node1 = edge.get(0); + QueryPlanNode node2 = edge.get(1); int[][] jp = JOINT_PATTERN; for (int i = 0; i < jp.length; i++) { @@ -57,20 +56,20 @@ private void setJType() { } } - @Override - public void estimate(List params) { - if (!(isParametersOK(params) && estimatable())) { - this.edge.setCost(IEstimate.NA_COST); + /** Assigns join selectivity from the shared variables and endpoint patterns. */ + public void estimate() { + if (!estimatable()) { + this.edge.setCost(SelectivityEstimator.NA_COST); return; } - QPGNode node1 = edge.get(0); - QPGNode node2 = edge.get(1); + QueryPlanNode node1 = edge.get(0); + QueryPlanNode node2 = edge.get(1); ExpType.Type tNode1 = node1.getType(); ExpType.Type tNode2 = node2.getType(); - //1. type of one of them is FILTER or VALUES or BIND, ne assign pas le weight - if (!QuerySorterConst.evaluable(tNode1) && !QuerySorterConst.evaluable(tNode2)) { + // Non-evaluable endpoint pairs receive the maximum cost. + if (!PlanningConstants.evaluable(tNode1) && !PlanningConstants.evaluable(tNode2)) { this.edge.setCost(MAX_COST); return; } @@ -100,16 +99,10 @@ public void estimate(List params) { @Override public String toString() { - return "QPGEdgeWeightModel{" + "nShare=" + nShare + ", jType=" + jType + '}'; + return "JoinCostModel{" + "nShare=" + nShare + ", jType=" + jType + '}'; } - @Override - public boolean isParametersOK(List params) { - return true; - } - - @Override - public final boolean estimatable() { - return QuerySorterConst.evaluable(this.edge.get(0).getType()) && QuerySorterConst.evaluable(this.edge.get(1).getType()); + public boolean estimatable() { + return PlanningConstants.evaluable(this.edge.get(0).getType()) && PlanningConstants.evaluable(this.edge.get(1).getType()); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGNodeCostModel.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/NodeCostModel.java similarity index 78% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGNodeCostModel.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/NodeCostModel.java index 42ccfb9a6..85341f700 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGNodeCostModel.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/NodeCostModel.java @@ -1,4 +1,4 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; import fr.inria.corese.core.next.query.impl.engine.model.Edge; import fr.inria.corese.core.next.query.impl.engine.model.ExpType; @@ -7,20 +7,21 @@ import java.util.ArrayList; import java.util.List; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.BOUND; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.LIST; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.NA; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.PREDICATE; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.UNBOUND; -import static fr.inria.corese.core.next.query.impl.engine.sorter.IEstimate.MAX_COST; -import static fr.inria.corese.core.next.query.impl.engine.sorter.IEstimate.MIN_COST_0; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.BOUND; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.LIST; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.NA; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.PREDICATE; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.UNBOUND; +import static fr.inria.corese.core.next.query.impl.engine.planning.SelectivityEstimator.MAX_COST; +import static fr.inria.corese.core.next.query.impl.engine.planning.SelectivityEstimator.MIN_COST_0; /** - * Class for constructing the pattern of a triple, including many parameters + * Describes bound positions and filter participation for one planning node. + * Equivalent patterns receive the same selectivity estimate. * * @author Fuqi Song, Wimmics Inria I3S */ -public class QPGNodeCostModel extends AbstractCostModel { +public final class NodeCostModel { //FV: filter, number of variables(in the triple pattern) appeared in all the filters // the more the better (less selectivity) @@ -33,13 +34,13 @@ public class QPGNodeCostModel extends AbstractCostModel { private static final int FF = 4; private static final int FV = 5; private static final int PARAMETER_LEN = 6; - //list of vairables appeared in the expression + // Variables occurring in the expression. List variables = new ArrayList<>(); private final int[] pattern = new int[PARAMETER_LEN]; - private final QPGNode node; + private final QueryPlanNode node; - public QPGNodeCostModel(QPGNode n, List bindings) { + public NodeCostModel(QueryPlanNode n, List bindings) { this.node = n; Node gNode; @@ -62,15 +63,18 @@ public QPGNodeCostModel(QPGNode n, List bindings) { } } - @Override - public void estimate(List params) { - if (!(isParametersOK(params) && estimatable())) { + /** + * Assigns the node's selectivity from its position among ranked pattern groups. + * + * @param size number of pattern groups, at least one + * @param index zero-based rank of this node's group + */ + public void estimate(int size, int index) { + if (!estimatable()) { this.node.setCost(Integer.MAX_VALUE); return; } - int size = (Integer) params.get(0); - int index = (Integer) params.get(1); //size == 1 means there is only one list, in which all nodes should have same costs if(size == 1){ @@ -95,20 +99,18 @@ private int getNodeType(Node n, List bindings) { } /** - * Set the paramters other than basic paremeters, only can be set after - * construction of the whole basic pattern graph + * Completes filter-dependent parameters after the pattern graph is constructed. * + * @param graph complete expression graph */ - public void setParameters(QPGraph graph) { + public void setParameters(QueryPlanGraph graph) { this.setFilterNumber(graph); - //other parameters can be added here if needed - //... } // FV: filter variables // FF: filter number - private void setFilterNumber(QPGraph graph) { - List nodes = graph.getAllNodes(ExpType.Type.FILTER); + private void setFilterNumber(QueryPlanGraph graph) { + List nodes = graph.getAllNodes(ExpType.Type.FILTER); int noOfFilter = 0; int noOfVariable = 0; @@ -117,7 +119,7 @@ private void setFilterNumber(QPGraph graph) { List variablesInFilters = new ArrayList<>(); //**2 check triple pattern has how many filters FF - for (QPGNode n : nodes) { + for (QueryPlanNode n : nodes) { List l = n.getExp().getFilter().getVariables(); variablesInFilters.addAll(l); @@ -175,13 +177,13 @@ public final int match(int[][] basicPatterns) { * @param bp basic patterns * @param ip interface producer */ - public static void sort(List patterns, final int[][] bp, final IProducerQP ip) { + public static void sort(List patterns, final int[][] bp, final QueryStatistics ip) { patterns.sort((m1, m2) -> compareModel(m1, m2, bp, ip)); } //order by selectivity asending(0-1) (more selective - less selective) @SuppressWarnings("java:S3776") - public static int compareModel(QPGNodeCostModel m1, QPGNodeCostModel m2, final int[][] bp, final IProducerQP ip) { + public static int compareModel(NodeCostModel m1, NodeCostModel m2, final int[][] bp, final QueryStatistics ip) { //GRAPH //rule 1: GRAPH has more priority than EDGE ExpType.Type type1 = m1.node.getExp().type(); @@ -253,15 +255,8 @@ public boolean isBound(List bindings, Node targetNode) { return false; } - @Override - public boolean isParametersOK(List params) { - return params != null && params.size() == 2 && params.get(0) instanceof Integer - && params.get(1) instanceof Integer; - } - - @Override public boolean estimatable() { - return QuerySorterConst.evaluable(this.node.getType()); + return PlanningConstants.evaluable(this.node.getType()); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/ISort.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/PatternOrderingStrategy.java similarity index 52% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/ISort.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/PatternOrderingStrategy.java index 09a35a1ef..f1a168b30 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/ISort.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/PatternOrderingStrategy.java @@ -1,15 +1,15 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; import java.util.List; /** - * Interface for sorting and rewriting the QPG nodes + * Selects a traversal order and rewrites an expression group in that order. * * @author Fuqi Song, WImmics Inria I3S */ -public interface ISort { +public interface PatternOrderingStrategy { /** * Sort the QPG node @@ -17,10 +17,10 @@ public interface ISort { * @param unsorted graph * @return List of sorted QPG Node */ - List sort(QPGraph unsorted); + List sort(QueryPlanGraph unsorted); /** * Rewrite the SPARQL exp according to give order of nodes */ - void rewrite(Exp exp, List nodes, int start); + void rewrite(Exp exp, List nodes, int start); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QuerySorterConst.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/PlanningConstants.java similarity index 83% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QuerySorterConst.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/PlanningConstants.java index 700b92d1a..65e83dcf8 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QuerySorterConst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/PlanningConstants.java @@ -1,15 +1,15 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; import fr.inria.corese.core.next.query.impl.engine.model.ExpType; /** - * Constants + * Binding-state and triple-position constants shared by query planning strategies. * * @author Fuqi Song, Wimmics Inria I3S */ -public final class QuerySorterConst { +public final class PlanningConstants { - private QuerySorterConst() { + private PlanningConstants() { // Utility class } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGEdge.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanEdge.java similarity index 74% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGEdge.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanEdge.java index 261c69a26..2b4a5c4bc 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGEdge.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanEdge.java @@ -1,31 +1,31 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; import java.util.List; /** - * Basic Query triple Pattern Graph edge used to connect two QPG nodes + * Connects two planning nodes and records their shared variables and join cost. * * @author Fuqi Song, Wimmics Inria I3S */ -public class QPGEdge { +public final class QueryPlanEdge { static final int BI_DIRECT = 10; static final int SIMPLE = 20; - private final QPGNode n1; - private final QPGNode n2; - private final AbstractCostModel costModel; + private final QueryPlanNode n1; + private final QueryPlanNode n2; + private final JoinCostModel costModel; private double cost = -1; private final List variables; private int type; - public QPGEdge(QPGNode n1, QPGNode n2) { + public QueryPlanEdge(QueryPlanNode n1, QueryPlanNode n2) { this.n1 = n1; this.n2 = n2; - //set list of shared vairables + // Record the variables shared by both endpoints. this.variables = n1.shared(n2); - this.costModel = new QPGEdgeCostModel(this); + this.costModel = new JoinCostModel(this); } public double getCost() { @@ -36,7 +36,7 @@ public void setCost(double cost) { this.cost = cost; } - public AbstractCostModel getCostModel() { + public JoinCostModel getCostModel() { return costModel; } @@ -56,7 +56,7 @@ public void setType(int type) { * Get one of the nodes in the edge * */ - public QPGNode get(int in) { + public QueryPlanNode get(int in) { if (in == 0) { return n1; } else if (in == 1) { @@ -69,7 +69,7 @@ public QPGNode get(int in) { * Get the other node in the edge * */ - public QPGNode get(QPGNode n) { + public QueryPlanNode get(QueryPlanNode n) { return n1.equals(n) ? n2 : n1; } @@ -99,7 +99,7 @@ public boolean equals(Object obj) { return false; } - final QPGEdge other = (QPGEdge) obj; + final QueryPlanEdge other = (QueryPlanEdge) obj; if (other.n1 == null || other.n2 == null) { return false; } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGraph.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanGraph.java similarity index 53% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGraph.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanGraph.java index 5cd26c675..7af4d2c5a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGraph.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanGraph.java @@ -1,4 +1,4 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; import fr.inria.corese.core.next.query.impl.engine.model.ExpType; import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; @@ -9,25 +9,24 @@ import java.util.Map; /** - * Query Pattern Graph (QPG) used to encapsulate a SPARQL statement and - * represent exp in a graph + * Represents a plannable expression group as nodes connected by shared variables. + * The mutable graph and its cost estimates belong to one planning operation. * * @author Fuqi Song, Wimmics Inria I3S */ -public class QPGraph { +public class QueryPlanGraph { // list of nodes - private final List nodes; + private final List nodes; // list of edges - private final List edges; + private final List edges; // identified bound values private final List bindings; - // data structure that used to represents the graph - // map(node, edges) - private Map> graph = null; + // Adjacency index from each node to its incident edges. + private Map> graph = null; - public QPGraph(List exps, List bindings) { + public QueryPlanGraph(List exps, List bindings) { this.bindings = bindings; this.nodes = new ArrayList<>(); this.edges = new ArrayList<>(); @@ -36,31 +35,24 @@ public QPGraph(List exps, List bindings) { createEdges(); } - // Encapsulate expression into BPGNode and add them to a list + // Preserve expression order when creating graph nodes. private void createNodes(List exps) { for (Exp ee : exps) { - nodes.add(new QPGNode(ee, this.bindings)); + nodes.add(new QueryPlanNode(ee, this.bindings)); } } //Create graph structure by finding variables sharing between nodes private void createEdges() { - // Graph Structure: - // Edge 1: connected edge11, edge 12, ... - // Edge 2: connected edge 21, edge 22, ...\ - // ... - // Filter 1: Connected edge 111, edge 112, ... - // Filter 2... - // ... - //values ... + // Retain the existing directed edge order for deterministic traversal. graph = new HashMap<>(); - for (QPGNode bpn1 : nodes) { - for (QPGNode bpn2 : nodes) { + for (QueryPlanNode bpn1 : nodes) { + for (QueryPlanNode bpn2 : nodes) { if (bpn1.equals(bpn2)) continue; //make sure not repeated if (bpn1.isShared(bpn2)) { - QPGEdge edge = new QPGEdge(bpn1, bpn2); + QueryPlanEdge edge = new QueryPlanEdge(bpn1, bpn2); edges.add(edge); createIndex(edge); } @@ -68,15 +60,15 @@ private void createEdges() { } } - private void createIndex(QPGEdge edge) { - QPGNode n1 = edge.get(0); - QPGNode n2 = edge.get(1); + private void createIndex(QueryPlanEdge edge) { + QueryPlanNode n1 = edge.get(0); + QueryPlanNode n2 = edge.get(1); //Node 1 if (graph.containsKey(n1)) { graph.get(n1).add(edge); } else { - List ledges = new ArrayList<>(); + List ledges = new ArrayList<>(); ledges.add(edge); graph.put(n1, ledges); } @@ -85,7 +77,7 @@ private void createIndex(QPGEdge edge) { if (graph.containsKey(n2)) { graph.get(n2).add(edge); } else { - List ledges = new ArrayList<>(); + List ledges = new ArrayList<>(); ledges.add(edge); graph.put(n2, ledges); } @@ -96,9 +88,10 @@ private void createIndex(QPGEdge edge) { * * @param edgeType type of edge: SIMPLE | BI_DIRECT */ - public List getEdges(int edgeType) { - List lEdges = new ArrayList<>(); - for (QPGEdge e : this.edges) { + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. + public List getEdges(int edgeType) { + List lEdges = new ArrayList<>(); + for (QueryPlanEdge e : this.edges) { if (e.getType() == edgeType) { lEdges.add(e); } @@ -111,13 +104,14 @@ public List getEdges(int edgeType) { * Get all edges linked to a given node * */ - public List getEdges(QPGNode node, int edgeType) { - List lEdges = new ArrayList<>(); + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. + public List getEdges(QueryPlanNode node, int edgeType) { + List lEdges = new ArrayList<>(); if (this.graph.get(node) == null) { return lEdges; } - for (QPGEdge e : this.graph.get(node)) { + for (QueryPlanEdge e : this.graph.get(node)) { if (e.getType() == edgeType) { lEdges.add(e); } @@ -130,12 +124,13 @@ public List getEdges(QPGNode node, int edgeType) { * Return the edges linked to the given node * */ - public List getEdges(QPGNode node) { + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. + public List getEdges(QueryPlanNode node) { return this.graph.get(node); } - public QPGEdge getEdge(QPGNode n1, QPGNode n2) { - for (QPGEdge e : this.graph.get(n1)) { + public QueryPlanEdge getEdge(QueryPlanNode n1, QueryPlanNode n2) { + for (QueryPlanEdge e : this.graph.get(n1)) { if (e.get(n1).equals(n2)) { return e; } @@ -148,7 +143,8 @@ public QPGEdge getEdge(QPGNode n1, QPGNode n2) { * Return list of nodes contained in the triple pattern graph * */ - public List getAllNodes() { + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. + public List getAllNodes() { return this.getAllNodes(ExpType.Type.EMPTY); } @@ -157,10 +153,11 @@ public List getAllNodes() { * * @param type EDGE, VALUES, FILTER, GRAPH, otherwise return all */ - public List getAllNodes(ExpType.Type type) { - if (QuerySorterConst.plannable(type)) { - List list = new ArrayList<>(); - for (QPGNode node : this.nodes) { + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. + public List getAllNodes(ExpType.Type type) { + if (PlanningConstants.plannable(type)) { + List list = new ArrayList<>(); + for (QueryPlanNode node : this.nodes) { if (node.getType() == type) { list.add(node); } @@ -176,13 +173,14 @@ public List getAllNodes(ExpType.Type type) { * @param n node * @return list */ - public List getLinkedNodes(QPGNode n) { - List l = new ArrayList<>(); + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. + public List getLinkedNodes(QueryPlanNode n) { + List l = new ArrayList<>(); if(!graph.containsKey(n)){ return l; } - for (QPGEdge e : graph.get(n)) { + for (QueryPlanEdge e : graph.get(n)) { l.add(e.get(n)); } return l; @@ -192,17 +190,18 @@ public List getLinkedNodes(QPGNode n) { * Get nodes linked to the given node * */ - public List getLinkedNodes(QPGNode n, boolean directed, boolean in) { + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. + public List getLinkedNodes(QueryPlanNode n, boolean directed, boolean in) { if (!directed) { return this.getLinkedNodes(n); } - List l = new ArrayList<>(); + List l = new ArrayList<>(); if (!graph.containsKey(n)) { return l; } - for (QPGEdge e : graph.get(n)) { + for (QueryPlanEdge e : graph.get(n)) { if (in) { if (e.get(1).equals(n)) { l.add(e.get(0)); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGNode.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanNode.java similarity index 90% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGNode.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanNode.java index 94ce6b234..26f12cc99 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QPGNode.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanNode.java @@ -1,4 +1,4 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; import fr.inria.corese.core.next.query.impl.engine.model.Edge; import fr.inria.corese.core.next.query.impl.engine.model.ExpType; @@ -11,35 +11,31 @@ import java.util.LinkedHashSet; import java.util.Set; import java.util.stream.Stream; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.OBJECT; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.PREDICATE; -import static fr.inria.corese.core.next.query.impl.engine.sorter.QuerySorterConst.SUBJECT; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.OBJECT; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.PREDICATE; +import static fr.inria.corese.core.next.query.impl.engine.planning.PlanningConstants.SUBJECT; /** - * The node for triple pattern graph, which encapsualtes an expression (contain - * an object exp) with cost + * Associates an engine expression with its variables and estimated planning cost. * * @author Fuqi Song, Wimmics Inria I3S */ -public class QPGNode { +public final class QueryPlanNode { // the expression that the node encapsulates private final Exp exp; private final ExpType.Type type; - private final QPGNodeCostModel costModel; + private final NodeCostModel costModel; private double cost = -1; - //the nested QPG in a QPG node, ex, GRAPH - //and for future extionson, ex, UNION - //private QPGraph nested = null; - public QPGNode(Exp exp, List bindings) { + public QueryPlanNode(Exp exp, List bindings) { this.exp = exp; this.type = exp.type(); - this.costModel = new QPGNodeCostModel(this, bindings); + this.costModel = new NodeCostModel(this, bindings); } - public QPGNodeCostModel getCostModel() { + public NodeCostModel getCostModel() { return costModel; } @@ -87,15 +83,15 @@ public Node getExpNode(int i) { * @param n BP node * @return true: shared; false: not share */ - public boolean isShared(QPGNode n) { + public boolean isShared(QueryPlanNode n) { return !this.shared(n).isEmpty(); } - public List shared(QPGNode n) { + public List shared(QueryPlanNode n) { return this.shared(this, n); } - public List shared(QPGNode bpn1, QPGNode bpn2) { + public List shared(QueryPlanNode bpn1, QueryPlanNode bpn2) { ExpType.Type type1 = bpn1.exp.type(); ExpType.Type type2 = bpn2.exp.type(); @@ -304,7 +300,7 @@ public String toString() { @Override public boolean equals(Object obj) { - if (obj instanceof QPGNode other) { + if (obj instanceof QueryPlanNode other) { return this.exp.equals(other.exp); } return false; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QuerySorter.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanner.java similarity index 84% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QuerySorter.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanner.java index 6594b7f9c..199be6830 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/QuerySorter.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryPlanner.java @@ -1,4 +1,4 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; import fr.inria.corese.core.next.query.impl.engine.model.ExpType; import fr.inria.corese.core.next.query.impl.engine.model.Expr; import fr.inria.corese.core.next.query.impl.engine.model.ExprType; @@ -8,38 +8,32 @@ import fr.inria.corese.core.next.query.impl.engine.pattern.Query; import fr.inria.corese.core.next.query.impl.engine.spi.Producer; -import fr.inria.corese.core.next.query.impl.engine.filter.Compile; +import fr.inria.corese.core.next.query.impl.engine.filter.FilterOptimizer; import java.util.ArrayList; import java.util.List; /** - * Sort query edges to optimize query processing Including exists {} edges - * Insert filters at place where variables are bound + * Coordinates expression ordering and places filters after their variables become + * available. Nested EXISTS patterns are planned with the surrounding query scope. * * @author Olivier Corby, Wimmics Inria I3S, 2014 * */ -public class QuerySorter implements ExpType { +public final class QueryPlanner implements ExpType { - private Sorter sort; - private Query query; - Compile compiler; + private final HeuristicPatternOrdering sort = new HeuristicPatternOrdering(); + private final Query query; + private final FilterOptimizer compiler; private Producer prod; + private Query statisticsQuery; + private Node statisticsGraph; // Assign sorter according to query plan profile - public QuerySorter(Query q) { + public QueryPlanner(Query q) { query = q; - compiler = new Compile(q); - setSort(q); - } - - void setSort(Query q) { - if (q.getPlanProfile() == Query.QP_HEURISTICS_BASED) { - sort = new SorterNew(); - } else { - sort = new Sorter(); - } + compiler = new FilterOptimizer(q); + statisticsQuery = q; } /** @@ -55,14 +49,14 @@ public void compile(Producer prod) { } /** - * Compile exists {} pattern when filter contains exists {}. + * FilterOptimizer exists {} pattern when filter contains exists {}. */ void compile(Filter f) { compile(f, new VString(), false); } /** - * Compile additional filters that may contain exists {}. + * FilterOptimizer additional filters that may contain exists {}. */ void modifier(Query q) { compile(q.getSelectFun()); @@ -83,10 +77,27 @@ void modifier(Query q) { * the List of variables already bound */ Exp compile(Exp exp, VString varList, boolean option) { + Query previousQuery = statisticsQuery; + Node previousGraph = statisticsGraph; + if (exp.isQuery() && exp.getQuery().isDatasetSpecified()) { + statisticsQuery = exp.getQuery(); + } + if (exp.isGraph()) { + statisticsGraph = exp.getGraphName(); + } + try { + return compileExpression(exp, varList, option); + } finally { + statisticsQuery = previousQuery; + statisticsGraph = previousGraph; + } + } + + private Exp compileExpression(Exp exp, VString varList, boolean option) { Type type = exp.type(); switch (type) { - case EDGE, PATH, XPATH, EVAL, NODE, GRAPHNODE: + case EDGE, XPATH, EVAL, NODE, GRAPHNODE: break; case FILTER, BIND: @@ -165,23 +176,12 @@ Exp queryPlan(Exp exp, VString lVar) { sortFilter(exp, lVar); break; - case Query.QP_HEURISTICS_BASED: - sort = new SorterNew(); - ((SorterNew) sort).sort(exp, lBind, getProducer()); - setBind(getQuery(), exp); - break; - - case Query.QP_BGP, Query.QP_DEFAULT: - // sort statements in connected order - sort.sort(getQuery(), exp, lVar, lBind); + case Query.QP_BGP, Query.QP_DEFAULT, Query.QP_HEURISTICS_BASED: + orderPatterns(exp, lVar, lBind); // move filters sortFilter(exp, lVar); setBind(getQuery(), exp); - if (getQuery().getPlanProfile() == Query.QP_BGP && - getQuery().getBgpGenerator() != null) { - exp = getQuery().getBgpGenerator().process(exp); - } break; default: @@ -191,6 +191,16 @@ Exp queryPlan(Exp exp, VString lVar) { } return exp; } + + private void orderPatterns(Exp exp, List variables, List bindings) { + if (getProducer() instanceof QueryStatistics statistics) { + sort.sort(statisticsQuery, statisticsGraph, exp, variables, bindings, statistics); + } else if (getQuery().getPlanProfile() == Query.QP_HEURISTICS_BASED) { + sort.sort(exp, bindings, getProducer()); + } else { + sort.sort(getQuery(), exp, variables, bindings); + } + } /** * Compute and record list of inscope variables * that may be bound to evaluate exp in an optimized way @@ -246,7 +256,7 @@ void compile(Filter f, VString lVar, boolean opt) { } /** - * Compile pattern of exists {} if any + * FilterOptimizer pattern of exists {} if any */ void compile(Expr exp, VString lVar, boolean opt) { if (exp.oper() == ExprType.EXIST) { @@ -447,6 +457,9 @@ public void setProducer(Producer prod) { } static class VString extends ArrayList { + @java.io.Serial + private static final long serialVersionUID = 1L; + void clear(int size) { if (size == 0) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryStatistics.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryStatistics.java new file mode 100644 index 000000000..df5a72db4 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/QueryStatistics.java @@ -0,0 +1,52 @@ +package fr.inria.corese.core.next.query.impl.engine.planning; + +import fr.inria.corese.core.next.query.impl.engine.model.Edge; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; + +/** + * Supplies optional planning statistics without opening statement cursors. + * Counts describe the state visible to the producer's storage session. They are + * estimates for planning, not a snapshot or a guarantee about later execution. + * + * @author Fuqi Song, WImmics Inria I3S + */ +public interface QueryStatistics { + + /** Sentinel for unavailable statistics; zero is a valid empty-pattern count. */ + long UNKNOWN_CARDINALITY = -1; + + /** + * Estimates an unbound triple pattern in its active dataset and graph scope. + * Implementations must not open a cursor or a transaction for this operation. + * + * @param query query supplying the active dataset + * @param graphNode enclosing GRAPH node, or {@code null} for the default graph + * @param edge triple pattern whose constants constrain the estimate + * @return nonnegative cardinality, or {@link #UNKNOWN_CARDINALITY} if unavailable + */ + default long estimateCardinality(Query query, Node graphNode, Edge edge) { + return UNKNOWN_CARDINALITY; + } + + /** + * Returns an optional total or distinct-term count for the heuristic model. + * + * @param type total or distinct subject/predicate/object count selector + * @return count, or {@code -1} when unavailable without collecting statistics + */ + default int getSize(int type) { + return -1; + } + + /** + * Returns an optional position-specific count for the heuristic model. + * + * @param node expression node to estimate + * @param type subject/predicate/object count selector + * @return count, or {@code -1} when unavailable + */ + default int getCount(QueryPlanNode node, int type) { + return -1; + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/IEstimate.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/SelectivityEstimator.java similarity index 61% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/IEstimate.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/SelectivityEstimator.java index b2bc6786d..e464f5b1e 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/IEstimate.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/SelectivityEstimator.java @@ -1,13 +1,13 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; import fr.inria.corese.core.next.query.impl.engine.spi.Producer; /** - * Interface for estimating the cost of nodes and edges in QPGraph + * Assigns selectivity costs to the nodes and edges of a planning graph. * * @author Fuqi Song, WImmics Inria I3S */ -public interface IEstimate { +public interface SelectivityEstimator { double MAX_COST = 1.0; //approximate minimum value, but not equal to 0 @@ -20,8 +20,7 @@ public interface IEstimate { * * @param plein graph * @param producer producer - * @param parameters parameters for different implementations */ - void estimate(QPGraph plein, Producer producer, Object parameters); + void estimate(QueryPlanGraph plein, Producer producer); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/package-info.java similarity index 58% rename from src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/package-info.java rename to src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/package-info.java index 253fcb714..146ac3244 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/package-info.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/planning/package-info.java @@ -1,4 +1,4 @@ /** * Query pattern optimization, heuristic reordering, and cost estimation models. */ -package fr.inria.corese.core.next.query.impl.engine.sorter; +package fr.inria.corese.core.next.query.impl.engine.planning; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/Mapping.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/Mapping.java index a1171b84a..fc6d65363 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/Mapping.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/Mapping.java @@ -1,5 +1,7 @@ package fr.inria.corese.core.next.query.impl.engine.solution; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopy; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopyable; import fr.inria.corese.core.next.query.impl.engine.eval.Eval; import fr.inria.corese.core.next.query.impl.engine.model.BindingContext; import fr.inria.corese.core.next.query.impl.engine.model.Edge; @@ -11,7 +13,6 @@ import fr.inria.corese.core.next.query.impl.engine.model.TripleStore; import fr.inria.corese.core.next.query.impl.engine.pattern.Query; -import fr.inria.corese.core.next.query.impl.engine.spi.Result; import fr.inria.corese.core.next.query.impl.engine.path.Path; import fr.inria.corese.core.next.query.impl.engine.eval.EnvironmentImpl; import fr.inria.corese.core.next.data.api.model.DatatypeValue; @@ -39,7 +40,7 @@ */ public final class Mapping extends EnvironmentImpl - implements Result, Pointerable> { + implements Pointerable>, PlanCopyable { private static final Logger LOGGER = LoggerFactory.getLogger(Mapping.class); @@ -67,6 +68,18 @@ public final class Mapping private BindingContext bindingContext; private Eval eval; + /** Copies the variable and value arrays of a static VALUES row. */ + @Override + public Mapping copyPlan(PlanCopy copies) { + Node[] variables = new Node[queryNodes.length]; + Node[] targets = new Node[targetNodes.length]; + for (int i = 0; i < variables.length; i++) variables[i] = copies.copy(queryNodes[i]); + for (int i = 0; i < targets.length; i++) targets[i] = copies.copy(targetNodes[i]); + Mapping target = Mapping.create(variables, targets); + copies.remember(this, target); + return target; + } + public Mapping() { init(emptyEdge, emptyEdge); init(emptyNode, emptyNode); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/MappingSet.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/MappingSet.java index dde7d4947..9bc5b124e 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/MappingSet.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/MappingSet.java @@ -1,7 +1,6 @@ package fr.inria.corese.core.next.query.impl.engine.solution; import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; -import fr.inria.corese.core.next.query.impl.engine.pattern.Query; import fr.inria.corese.core.next.query.impl.engine.model.Node; @@ -28,7 +27,7 @@ * * @author Olivier Corby, Wimmics INRIA I3S, 2018 */ -public class MappingSet { +public final class MappingSet { Mappings map; Exp exp; MappingSet set1; @@ -39,18 +38,16 @@ public class MappingSet { boolean isBound = false; - public MappingSet(Query q, Exp exp, MappingSet s1, MappingSet s2) { + public MappingSet(Exp exp, MappingSet s1, MappingSet s2) { this.exp = exp; this.set1 = s1; this.set2 = s2; - setQuery(q); } - public MappingSet(Query q, Mappings mappings) { + public MappingSet(Mappings mappings) { this.map = mappings; union = new HashMap<>(); intersection = new HashMap<>(); - setQuery(q); process(); } @@ -226,10 +223,6 @@ public Mappings prepareMappingsRest(Exp expression) { return getMappings(); } - public void setQuery(Query query) { - // query is kept for compatibility - } - class Iterate implements Iterator { int n; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/Mappings.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/Mappings.java index f37ed7bcd..ece9db151 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/Mappings.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/solution/Mappings.java @@ -1,5 +1,7 @@ package fr.inria.corese.core.next.query.impl.engine.solution; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopy; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopyable; import fr.inria.corese.core.next.query.api.exception.QueryTypeErrorException; import fr.inria.corese.core.next.query.impl.engine.eval.Eval; import fr.inria.corese.core.next.query.impl.engine.eval.PointerObject; @@ -37,7 +39,7 @@ * @author Olivier Corby, Edelweiss, INRIA 2009 */ public final class Mappings extends PointerObject - implements Comparator, Iterable { + implements Comparator, Iterable, PlanCopyable { private static final String NL = "\n"; @@ -55,7 +57,6 @@ public final class Mappings extends PointerObject // if true, store all Mapping of the group boolean isListGroup = false; boolean sortWithDesc = true; - List reject; KgramEventDispatcher manager; int count = 0; private Query query; @@ -90,6 +91,15 @@ public final class Mappings extends PointerObject // limit number of results to be displayed private int display = Integer.MAX_VALUE; + /** Copies a static VALUES table into independent row containers. */ + @Override + public Mappings copyPlan(PlanCopy copies) { + Mappings target = new Mappings(); + copies.remember(this, target); + for (Mapping row : list) target.add(copies.copy(row)); + return target; + } + public Mappings() { list = new ArrayList<>(); link = new ArrayList<>(); @@ -858,42 +868,6 @@ void having(Evaluator eval, Exp exp, Memory mem, Producer p, Group g) { } } - /** - * Template perform additionnal group_concat(?out) - */ - public void template(Evaluator eval, Memory mem, Producer p) { - template(eval, getQuery(), mem, p); - } - - void template(Evaluator eval, Query q, Memory mem, Producer p) { - if (q.isTemplate() && size() > 0 && !(isFake() && q.isTransformationTemplate())) { - // fake in transformation template -> fail - // fake in query template -> not fail - apply(eval, q.getTemplateGroup(), mem, p); - } - } - - /** - * Template perform additionnal group_concat(?out) - */ - public Node apply(Evaluator eval, Exp exp, Memory memory, Producer p) { - Mapping firstMap = get(0); - // bind the Mapping in memory to retrieve group by variables - memory.aggregate(firstMap); - if (size() == 1) { - Node node = eval(exp.getFilter().getExp().getExp(0).getFilter(), eval, memory, p); - if (node != null) { - // When node is null, go to aggregate below because we want it to be uniform - // whether there is one or several results - return node; - } - } - - Node node = eval(exp.getFilter(), eval, memory, p); - memory.pop(firstMap); - return node; - } - /** * process group by leave one Mapping within each group */ 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 72a1c5159..0a0f9e9d6 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 @@ -21,7 +21,6 @@ import fr.inria.corese.core.next.query.impl.engine.event.KgramEventDispatcher; import fr.inria.corese.core.next.query.impl.engine.path.Path; -import fr.inria.corese.core.next.query.impl.engine.eval.ApproximateSearchEnv; import fr.inria.corese.core.next.data.api.model.DatatypeValue; import java.util.ArrayList; @@ -30,7 +29,13 @@ import java.util.Map; /** - * Node and Edge binding stacks for KGRAM evaluator + * Mutable binding stacks and solution construction for one evaluation. + * + *

    Each query or nested evaluation owns its node arrays and binding counters. + * Push/pop pairs track the stack level at which a variable first became bound, + * allowing the evaluator to backtrack without rebuilding its environment. + * Instances are confined to the evaluation thread; cancellation is observed by + * the owning evaluator rather than by sharing these arrays with another thread.

    * * @author Olivier Corby, Edelweiss, INRIA 2009 */ @@ -76,7 +81,6 @@ public class Memory extends PointerObject implements Environment { private DatatypeValue detail; private boolean isEdge; private BindingContext bindingContext; - private ApproximateSearchEnv appxSearchEnv; public Memory() { } @@ -85,7 +89,6 @@ public Memory(Matcher m, Evaluator e) { match = m; eval = e; bnode = new HashMap<>(); - this.appxSearchEnv = new ApproximateSearchEnv(); } @Override @@ -341,170 +344,151 @@ void copyInto(Node qNode, Memory mem) { } } - public Mapping store(Query q, Producer p, boolean subEval) { - return store(q, p, subEval, false); - } - /** - * subEval = true : result of statement such as minus/optional etc - * in this case: no select exp, no order by, no group by, etc - * subEval = false: main or nested select query. + * Copies the current bindings into one solution, evaluating projection and + * ordering expressions only for a complete query result. + * + *

    The evaluator owns this mutable environment on its execution thread. + * Temporary SELECT bindings are removed before the next solution; the returned + * mapping owns its node arrays and retains the RDF values themselves.

    + * + * @param query query whose projection and ordering apply + * @param producer producer used to evaluate expressions and copy recorded edges + * @param subEvaluation whether the solution belongs to an intermediate graph pattern + * @return captured solution, or null when a projection conflicts with a binding */ - @SuppressWarnings("java:S3776") // Legacy KGRAM solution construction algorithm assembling Mapping results from memory state - Mapping store(Query q, Producer p, boolean subEval, boolean func) { - boolean complete = !q.getGlobalQuery().isAlgebra(); - - Node detailNode = null; - if (getReport() != null) { - // draft: set service report as variable value - // use case: xt:sparql() return map with report - // PluginImpl sparql() record report in Environment - // detailNode is defined by ASTParser with @report metadata - detailNode = getQuery().getSelectNode(SERVICE_REPORT_ZERO); - if (detailNode != null) { - push(detailNode, (Node) getReport()); - } + public Mapping store(Query query, Producer producer, boolean subEvaluation) { + Node reportNode = bindServiceReport(); + int capacity = solutionNodeCapacity(query, subEvaluation); + Edge[] queryEdgeCopies = isEdge ? new Edge[nbEdge] : emptyEdges; + Edge[] targetEdgeCopies = isEdge ? new Edge[nbEdge] : emptyEntities; + copySolutionEdges(producer, queryEdgeCopies, targetEdgeCopies); + + Node[] queryNodeCopies = new Node[capacity]; + Node[] targetNodeCopies = new Node[capacity]; + int boundNodes = copySolutionNodes(queryNodeCopies, targetNodeCopies); + Node[] orderByNodes = new Node[query.getOrderBy().size()]; + Node[] groupByNodes = new Node[query.getGroupBy().size()]; + + if (!subEvaluation && !projectSolution(query, producer, queryNodeCopies, targetNodeCopies, boundNodes)) { + return null; } + Mapping solution = new Mapping(queryEdgeCopies, targetEdgeCopies, queryNodeCopies, targetNodeCopies); + mapping = solution; + if (!subEvaluation) { + solution.init(); + // Ordering expressions may inspect the projected solution through the environment. + orderGroup(query.getOrderBy(), orderByNodes, producer); + orderGroup(query.getGroupBy(), groupByNodes, producer); + popProjectionBindings(query); + } + if (reportNode != null) { + pop(reportNode); + setReport(null); + } + solution.setOrderBy(orderByNodes); + solution.setGroupBy(groupByNodes); + clear(); + mapping = null; + return solution; + } - int nb = nbNode; - if (!subEval && complete) { - // select (exp as var) it may happen that var is already bound in - // memory (bindings, subquery), so we should not allocate a - // supplementary cell for var in Mapping node array - for (Exp selectFunExp : q.getSelectFun()) { - if (selectFunExp.getFilter() != null && !isBound(selectFunExp.getNode())) { - nb++; - } - } + private Node bindServiceReport() { + if (getReport() == null) { + return null; } - Edge[] qedge = emptyEdges; - Edge[] tedge = emptyEntities; - Node[] qnode = new Node[nb]; - Node[] tnode = new Node[nb]; - // order by - Node[] snode = new Node[q.getOrderBy().size()]; - Node[] gnode = new Node[q.getGroupBy().size()]; + Node reportNode = getQuery().getSelectNode(SERVICE_REPORT_ZERO); + if (reportNode != null) { + push(reportNode, (Node) getReport()); + } + return reportNode; + } - int n = 0; - int i = 0; - if (isEdge) { - qedge = new Edge[nbEdge]; - tedge = new Edge[nbEdge]; - for (Edge edge : qEdges) { - if (edge != null) { - qedge[n] = edge; - - tedge[n] = p.copy(result[i]); - n++; + private int solutionNodeCapacity(Query query, boolean subEvaluation) { + int capacity = nbNode; + if (!subEvaluation) { + for (Exp projection : query.getSelectFun()) { + if (projection.getFilter() != null && !isBound(projection.getNode())) { + capacity++; } - i++; } } + return capacity; + } - n = 0; - i = 0; - for (Node node : qNodes) { - if (node != null) { - qnode[n] = node; - tnode[n] = nodes[i]; - n++; + private void copySolutionEdges(Producer producer, Edge[] queryEdgeCopies, Edge[] targetEdgeCopies) { + if (!isEdge) { + return; + } + int targetIndex = 0; + for (int sourceIndex = 0; sourceIndex < qEdges.length; sourceIndex++) { + if (qEdges[sourceIndex] != null) { + queryEdgeCopies[targetIndex] = qEdges[sourceIndex]; + targetEdgeCopies[targetIndex] = producer.copy(result[sourceIndex]); + targetIndex++; } - i++; } + } - Mapping map = null; - - if (complete) { - if (subEval) { - // statement e.g. minus/optional/union - if (func) { - orderGroup(q.getOrderBy(), snode, p); - orderGroup(q.getGroupBy(), gnode, p); - } - } else { - // main query or nested query - int count = 0; - for (Exp e : q.getSelectFun()) { - - Filter f = e.getFilter(); - - if (f != null) { - // select fun(?x) as ?y - Node node = null; - boolean isBound = isBound(e.getNode()); - - if (!e.isAggregate()) { - - - node = evaluateExpressionNode(f, p); - kgram.getVisitor().select(kgram, f.getExp(), node == null ? null : node.getDatatypeValue()); - // bind fun(?x) as ?y - boolean success = push(e.getNode(), node); - if (success) { - count++; - } else { - // use case: var was already bound and there is a select (exp as var) - // and the two values of var are different - // pop previous exp nodes and return null - int j = 0; - - for (Exp ee : q.getSelectFun()) { - // pop previous exp nodes if any - if (j >= count) { - // we have poped all exp nodes - return null; - } - - if (ee.getFilter() != null) { - pop(ee.getNode()); - j++; - } - } - } - } - - if (!isBound) { - // use case: select (exp as var) where var is already bound - qnode[n] = e.getNode(); - tnode[n] = node; - n++; - } - } - } + private int copySolutionNodes(Node[] queryNodeCopies, Node[] targetNodeCopies) { + int targetIndex = 0; + for (int sourceIndex = 0; sourceIndex < qNodes.length; sourceIndex++) { + if (qNodes[sourceIndex] != null) { + queryNodeCopies[targetIndex] = qNodes[sourceIndex]; + targetNodeCopies[targetIndex] = nodes[sourceIndex]; + targetIndex++; + } + } + return targetIndex; + } - map = new Mapping(qedge, tedge, qnode, tnode); - mapping = map; - map.init(); - // order/group by may access mapping with xt:result() - orderGroup(q.getOrderBy(), snode, p); - orderGroup(q.getGroupBy(), gnode, p); - - for (Exp e : q.getSelectFun()) { - Filter f = e.getFilter(); - if (f != null && !e.isAggregate()) { - // pop fun(?x) as ?y - pop(e.getNode()); - } + private boolean projectSolution(Query query, Producer producer, Node[] queryNodeCopies, + Node[] targetNodeCopies, int targetIndex) { + int pushedBindings = 0; + for (Exp projection : query.getSelectFun()) { + Filter filter = projection.getFilter(); + if (filter == null) { + continue; + } + boolean previouslyBound = isBound(projection.getNode()); + Node value = null; + if (!projection.isAggregate()) { + value = evaluateExpressionNode(filter, producer); + kgram.getVisitor().select(kgram, filter.getExp(), value == null ? null : value.getDatatypeValue()); + if (!push(projection.getNode(), value)) { + popProjectionPrefix(query, pushedBindings); + return false; } + pushedBindings++; + } + if (!previouslyBound) { + queryNodeCopies[targetIndex] = projection.getNode(); + targetNodeCopies[targetIndex] = value; + targetIndex++; } } + return true; + } - if (map == null) { - map = new Mapping(qedge, tedge, qnode, tnode); - mapping = map; + private void popProjectionPrefix(Query query, int count) { + int popped = 0; + for (Exp projection : query.getSelectFun()) { + if (popped >= count) { + return; + } + if (projection.getFilter() != null) { + pop(projection.getNode()); + popped++; + } } + } - if (detailNode != null) { - pop(detailNode); - setReport(null); + private void popProjectionBindings(Query query) { + for (Exp projection : query.getSelectFun()) { + if (projection.getFilter() != null && !projection.isAggregate()) { + pop(projection.getNode()); + } } - - map.setOrderBy(snode); - map.setGroupBy(gnode); - clear(); - mapping = null; - - return map; } @Override @@ -1141,15 +1125,6 @@ public boolean hasBind() { return bindingContext != null && bindingContext.hasBind(); } - @Override - public ApproximateSearchEnv getAppxSearchEnv() { - return this.appxSearchEnv; - } - - public void setAppxSearchEnv(ApproximateSearchEnv appxEnv) { - this.appxSearchEnv = appxEnv; - } - /** * List of variable binding * diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/AbstractCostModel.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/AbstractCostModel.java deleted file mode 100644 index 782230cde..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/AbstractCostModel.java +++ /dev/null @@ -1,35 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; - -import java.util.List; - -/** - * CostModel.java - * Cost model used to estimate the cost for node or edge in a QPG - * - * @author Fuqi Song, Wimmics Inria I3S - */ -public abstract class AbstractCostModel { - - - /** - * Abstract method for estimating the cost using the model - * - */ - public abstract void estimate(List params); - - - /** - * Check if the paramters for method estimate(List params) are ok - * - * @return true:ok, false: not ok - */ - public abstract boolean isParametersOK(List params); - - /** - * Check whether the cost can be estimated using the model - * - * @return true:ok, false: not ok - */ - public abstract boolean estimatable(); - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/IProducerQP.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/IProducerQP.java deleted file mode 100644 index 1e892f12e..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/sorter/IProducerQP.java +++ /dev/null @@ -1,24 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.sorter; - -/** - * Interface for calling Producer from kgraph, these methods are implemented - * without the needs of stats data, only depending on the structure/index of - * graph - * - * @author Fuqi Song, WImmics Inria I3S - */ -public interface IProducerQP { - - /** - * Return the size of triples according to different types - * - * @param type: ALL, return the number of all triples in the graph - * SUBJECT( PREDICATE| OBJECT): return the number of disinct SUBJECT( PREDICATE| OBJECT) - */ - int getSize(int type); - - /** - * Return the count of triples according the given type - */ - int getCount(QPGNode n, int type); -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/AST.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/AST.java deleted file mode 100644 index 10df4a642..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/AST.java +++ /dev/null @@ -1,19 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.spi; - -/** - * Olivier Corby - Wimmics INRIA I3S - 2020 - */ -public interface AST { - - boolean isSelect(); - - boolean isConstruct(); - - boolean isUpdate(); - - boolean isInsert(); - - boolean isDelete(); - - boolean hasMetadata(String name); -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/DistributedQueryPlanFactory.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/DistributedQueryPlanFactory.java deleted file mode 100644 index 58939e397..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/DistributedQueryPlanFactory.java +++ /dev/null @@ -1,12 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.spi; - -import fr.inria.corese.core.next.query.impl.engine.pattern.BgpGenerator; - -/** - * @author corby - */ -public interface DistributedQueryPlanFactory { - - BgpGenerator instance(); - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Environment.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Environment.java index faf7b3d0b..34731af25 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Environment.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Environment.java @@ -13,7 +13,6 @@ import fr.inria.corese.core.next.query.impl.engine.model.Node; import fr.inria.corese.core.next.query.impl.engine.event.KgramEventDispatcher; import fr.inria.corese.core.next.query.impl.engine.path.Path; -import fr.inria.corese.core.next.query.impl.engine.eval.ApproximateSearchEnv; import fr.inria.corese.core.next.data.api.model.DatatypeValue; import java.util.Map; @@ -129,7 +128,6 @@ default void setGraphNode(Node n) { Node get(Expr varExpr); - ApproximateSearchEnv getAppxSearchEnv(); Eval getEval(); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Graphable.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Graphable.java deleted file mode 100644 index a2489b6a5..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Graphable.java +++ /dev/null @@ -1,15 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.spi; - -/** - * Olivier Corby - Wimmics INRIA I3S - 2014 - */ -public interface Graphable { - - String toGraph(); - - Object getGraph(); - - void setGraph(Object obj); - - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Plugin.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Plugin.java deleted file mode 100644 index befa1f5e2..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Plugin.java +++ /dev/null @@ -1,15 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.spi; - -import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; - -/** - * Draft KGRAM Plugin for EXTERN expression. - * KGRAM exec this function for EXTERN expression. - * - * @author Olivier Corby, Edelweiss, INRIA 2010 - */ -public interface Plugin { - - void exec(Exp exp, Environment env, int n); - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Producer.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Producer.java index 6d759cf38..2ac319644 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Producer.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Producer.java @@ -4,13 +4,11 @@ import fr.inria.corese.core.next.query.impl.engine.model.Edge; import fr.inria.corese.core.next.query.impl.engine.model.Graph; import fr.inria.corese.core.next.query.impl.engine.model.Node; -import fr.inria.corese.core.next.query.impl.engine.model.Regex; import fr.inria.corese.core.next.data.api.model.DatatypeValue; import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; import fr.inria.corese.core.next.query.impl.engine.pattern.Query; -import fr.inria.corese.core.next.query.impl.engine.eval.SparqlException; import java.util.List; @@ -86,63 +84,7 @@ default void closeEdges(Iterable edges) { // Most producers return in-memory collections without owned resources. } - Mappings getMappings(Node gNode, List from, Exp exp, Environment env) throws SparqlException; - - /* - * * ************** PATH ************* - */ - /** - * Start the processing of a path instruction with qEdge as pseudo query - * edge. index is the index of the node in qEdge where to start path. It is - * the index of the node that we want to enumerate in getNodes() - * - */ - void initPath(Edge qEdge, int index); - - /** - * Return nodes for Zero length path - * - * @param gNode The graph node where to get nodes (may be null) - * @param from from or from named, may be empty - * @param edge Pseudo query edge for path - * @param env The binding environment - * @param exp either property name or a ! (pname | pname) or null - * @param index of the node to return - * @return Iterable of start nodes for exp - *

    - * SPARQL 1.1 requires: ZeroLengthPath match all nodes of (current) graph, - * including Literals If the argument is a constant, it matches even if it - * is not a node of the graph - */ - Iterable getNodes(Node gNode, List from, Edge edge, Environment env, - List exp, int index); - - /** - * Return candidate edges for a path step for an elementary regex exp. Edge - * start at start Node, start is the node at index. Hence start can be - * edge.getNode(0) as well as edge.getNode(1) - *

    - * isInverse = true means consider also target edge nodes in reverse order - * query: start = Bob ; ?a ^foaf:knows ?b target: Jack foaf:knows Bob return - * edge above because we consider also Bob foaf:knows Jack This is - * orthogonal to index = 1, in which case we also consider nodes in inverse - * order - * - * @param gNode The start node of edges - * @param from from or from named, may be empty - * @param qEdge Pseudo query edge for path - * @param env The binding environment - * @param exp either property name or a ! (pname | pname) - * @param src current source if any - * @param start The start node for current edge - * @param index of the start node in edge exp.isInverse() authorize to - * consider nodes in reverse order (as if the symmetric relation would - * exist) - * @return Iterable of start nodes for exp - */ - @SuppressWarnings("java:S107") // Core KGRAM Producer interface method requires 8 execution parameters - Iterable getEdges(Node gNode, List from, Edge qEdge, Environment env, - Regex exp, Node src, Node start, int index); + Mappings getMappings(Node gNode, List from, Exp exp, Environment env); /* * ************** value to node ************* diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Provider.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Provider.java index 4d045988f..271d17ac5 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Provider.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Provider.java @@ -4,7 +4,6 @@ import fr.inria.corese.core.next.query.impl.engine.eval.Eval; import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; -import fr.inria.corese.core.next.query.impl.engine.eval.SparqlException; /** * Service Provider @@ -12,8 +11,7 @@ * @author Olivier Corby, Edelweiss, INRIA 2011 */ public interface Provider { - default Mappings service(Node serv, Exp exp, Mappings map, Eval eval) - throws SparqlException { + default Mappings service(Node serv, Exp exp, Mappings map, Eval eval) { return null; } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Result.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Result.java deleted file mode 100644 index 85030bd8d..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Result.java +++ /dev/null @@ -1,16 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.spi; - -import fr.inria.corese.core.next.query.impl.engine.model.Node; - -/** - * Interface to KGRAM elementary result (a mapping) - * - * @author corby - */ -public interface Result { - - Node getNode(String name); - - Node getNode(Node node); - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Results.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Results.java deleted file mode 100644 index 75dba17e6..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/Results.java +++ /dev/null @@ -1,18 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.spi; - -import fr.inria.corese.core.next.query.impl.engine.model.Node; - -import java.util.List; - -/** - * Interface to KGRAM results (mappings) - * - * @author corby - */ -public interface Results extends Iterable { - - List getSelect(); - - int size(); - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/SPARQLEngine.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/SPARQLEngine.java deleted file mode 100644 index 829533fc2..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/spi/SPARQLEngine.java +++ /dev/null @@ -1,23 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.spi; - -import fr.inria.corese.core.next.query.impl.engine.model.Node; -import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; -import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; -import fr.inria.corese.core.next.query.impl.engine.pattern.Query; -import fr.inria.corese.core.next.query.impl.engine.eval.SparqlException; - -/** - * @author corby - */ -public interface SPARQLEngine { - - Mappings eval(Query q, Mapping m, Producer p) throws SparqlException; - - Mappings eval(Node gNode, Query q, Mapping m, Producer p) throws SparqlException; - - // within a lock where query processing has ended and where we can process another query - boolean isSynchronized(); - - void setSynchronized(boolean b); - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/ProducerDefault.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/ProducerDefault.java index 8cc7591f6..40f7564f8 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/ProducerDefault.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/ProducerDefault.java @@ -4,7 +4,6 @@ import fr.inria.corese.core.next.query.impl.engine.model.Edge; import fr.inria.corese.core.next.query.impl.engine.model.Graph; import fr.inria.corese.core.next.query.impl.engine.model.Node; -import fr.inria.corese.core.next.query.impl.engine.model.Regex; import fr.inria.corese.core.next.data.api.model.DatatypeValue; import fr.inria.corese.core.next.query.impl.engine.spi.Environment; @@ -37,12 +36,6 @@ public Iterable getEdges(Node node, List from, Edge edge, return new ArrayList<>(); } - @Override - public Iterable getEdges(Node gNode, List from, Edge edge, Environment env, Regex exp, - Node src, Node start, - int index) { - return new ArrayList<>(); - } @Override public Iterable getGraphNodes(Node node, List from, @@ -50,11 +43,6 @@ public Iterable getGraphNodes(Node node, List from, return new ArrayList<>(); } - @Override - public void initPath(Edge edge, int index) { - // No-op by default in ProducerDefault - } - @Override public Node getNode(Object value) { return null; @@ -71,10 +59,6 @@ public boolean isBindable(Node node) { return false; } - @Override - public Iterable getNodes(Node gNode, List from, Edge edge, Environment env, List exp, int index) { - return new ArrayList<>(); - } @Override public boolean isProducer(Node node) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageBindingEnvironment.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageBindingEnvironment.java index f5a7fe369..f8b974fe3 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageBindingEnvironment.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageBindingEnvironment.java @@ -1,7 +1,6 @@ package fr.inria.corese.core.next.query.impl.engine.storage; import fr.inria.corese.core.next.data.api.model.DatatypeValue; -import fr.inria.corese.core.next.query.impl.engine.eval.ApproximateSearchEnv; import fr.inria.corese.core.next.query.impl.engine.eval.Eval; import fr.inria.corese.core.next.query.impl.engine.event.KgramEventDispatcher; import fr.inria.corese.core.next.query.impl.engine.event.ProcessVisitor; @@ -187,11 +186,6 @@ public Node get(Expr varExpr) { return delegate == null ? null : delegate.get(varExpr); } - @Override - public ApproximateSearchEnv getAppxSearchEnv() { - return delegate == null ? null : delegate.getAppxSearchEnv(); - } - @Override public Eval getEval() { return delegate == null ? null : delegate.getEval(); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageManagerProducer.java b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageManagerProducer.java index e06014c22..1262e909b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageManagerProducer.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/engine/storage/StorageManagerProducer.java @@ -5,15 +5,14 @@ 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.query.api.exception.UnsupportedQueryFeatureException; import fr.inria.corese.core.next.query.impl.engine.model.Edge; import fr.inria.corese.core.next.query.impl.engine.model.Node; import fr.inria.corese.core.next.query.impl.engine.model.NodeImpl; -import fr.inria.corese.core.next.query.impl.engine.model.Regex; import fr.inria.corese.core.next.query.impl.engine.path.NativePropertyPathEvaluator; import fr.inria.corese.core.next.query.impl.engine.path.PropertyPathEdge; import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; import fr.inria.corese.core.next.query.impl.engine.pattern.Query; +import fr.inria.corese.core.next.query.impl.engine.planning.QueryStatistics; import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; import fr.inria.corese.core.next.query.impl.engine.spi.Environment; import fr.inria.corese.core.next.storage.api.StorageManager; @@ -41,7 +40,7 @@ * then adapting returned RDF statements back into KGRAM edges. It does not decide the final RDF-term * matching policy; that remains the matcher responsibility.

    */ -public final class StorageManagerProducer extends ProducerDefault { +public class StorageManagerProducer extends ProducerDefault implements QueryStatistics { private final StorageManager storage; private final Set cursors = Collections.newSetFromMap(new IdentityHashMap<>()); @@ -56,6 +55,38 @@ public StorageManagerProducer(StorageManager storage) { this.storage = Objects.requireNonNull(storage, "storage"); } + /** + * Counts a pattern directly in the session-visible indexes. No cursor or + * transaction is created. Complexity depends on the selected index prefix and + * number of contexts; a merged default graph count is an upper bound because + * identical triples in different graphs are counted separately. + * + * @param query query supplying dataset restrictions + * @param graphNode enclosing graph, or {@code null} for the default graph + * @param edge triple pattern to estimate + * @return matching statement count, or unknown for paths and variable graphs + */ + @Override + public long estimateCardinality(Query query, Node graphNode, Edge edge) { + if (edge instanceof PropertyPathEdge || graphNode != null && graphNode.isVariable()) { + return UNKNOWN_CARDINALITY; + } + checkInterrupted(); + List contexts = query.getFrom(graphNode); + if (query.isDatasetSpecified() && contexts.isEmpty()) { + return 0; + } + var pattern = StoragePatternTranslator.translate(graphNode, contexts, edge, null); + if (pattern.noMatch()) { + return 0; + } + try { + return storage.queries().count(pattern.statementPattern()); + } catch (UnsupportedOperationException unavailable) { + return UNKNOWN_CARDINALITY; + } + } + @Override public Iterable getEdges(Node graphNode, List from, Edge queryEdge, Environment environment) { Objects.requireNonNull(queryEdge, "queryEdge"); @@ -128,7 +159,7 @@ private EdgeCursor(Stream stream, boolean deduplicate) { this.seen = deduplicate && !distinct ? new HashSet<>() : null; statements = Spliterators.iterator(source); } catch (RuntimeException | Error failure) { - stream.close(); + closeAfterFailure(stream::close, failure); throw failure; } } @@ -158,7 +189,7 @@ public synchronized boolean hasNext() { close(); return false; } catch (RuntimeException | Error failure) { - close(); + closeAfterFailure(this::close, failure); throw failure; } } @@ -175,7 +206,7 @@ public synchronized Edge next() { try { return new StorageManagerEdge(statement); } catch (RuntimeException | Error failure) { - close(); + closeAfterFailure(this::close, failure); throw failure; } } @@ -201,6 +232,16 @@ public synchronized void close() { } } + /** Preserves the original traversal failure when a close callback also fails. */ + @SuppressWarnings("java:S1181") // Fatal failures retain their identity after resource cleanup. + private static void closeAfterFailure(Runnable release, Throwable failure) { + try { + release.run(); + } catch (RuntimeException | Error cleanup) { + if (cleanup != failure) failure.addSuppressed(cleanup); + } + } + private static void checkInterrupted() { if (Thread.currentThread().isInterrupted()) { throw new QueryEvaluationException("Query evaluation was interrupted"); @@ -266,20 +307,6 @@ public Iterable getGraphNodes(Node graphNode, List from, Environment return nodes; } - @Override - public Iterable getEdges( - Node graphNode, - List from, - Edge queryEdge, - Environment environment, - Regex regex, - Node source, - Node start, - int index) { - throw new UnsupportedQueryFeatureException( - "Property path edge enumeration is not supported yet by StorageManagerProducer"); - } - @Override public void start(Query query) { // Cursors are registered when their iterators are opened. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/query/AbstractCoreseQuery.java b/src/main/java/fr/inria/corese/core/next/query/impl/query/AbstractCoreseQuery.java index abc1dae48..6bab6e9c0 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/query/AbstractCoreseQuery.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/query/AbstractCoreseQuery.java @@ -2,6 +2,11 @@ import fr.inria.corese.core.next.data.api.term.Value; import fr.inria.corese.core.next.query.api.Query; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecution; +import fr.inria.corese.core.next.query.impl.engine.diagnostics.ExecutionDiagnostics; +import fr.inria.corese.core.next.query.impl.sparql.execution.SparqlQueryExecutor; +import fr.inria.corese.core.next.query.impl.sparql.execution.PreparedQueryPlan; +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; import fr.inria.corese.core.next.query.api.dataset.Dataset; import fr.inria.corese.core.next.query.api.result.Binding; import fr.inria.corese.core.next.query.api.result.BindingSet; @@ -25,15 +30,51 @@ abstract class AbstractCoreseQuery implements Query { private final String queryString; private final Runnable executionGuard; + private final SparqlQueryExecutor executor; + private final PreparedQueryPlan preparedPlan; private final LinkedHashMap bindings = new LinkedHashMap<>(); private Dataset dataset; private Duration timeout = Duration.ZERO; - protected AbstractCoreseQuery(String queryString, Runnable executionGuard) { + protected AbstractCoreseQuery(String queryString, Runnable executionGuard, SparqlQueryExecutor executor, QueryAst parsedQuery) { this.queryString = Objects.requireNonNull(queryString, "queryString"); this.executionGuard = Objects.requireNonNull(executionGuard, "executionGuard"); + this.executor = Objects.requireNonNull(executor, "executor"); + this.preparedPlan = executor.prepare(queryString, parsedQuery); } + /** Returns this prepared query's compilation cache, never shared with another prepared query. */ + protected final PreparedQueryPlan preparedPlan() { + return preparedPlan; + } + + @Override + public final T evaluate() { + checkExecutable(); + return evaluate(executor); + } + + @Override + public final QueryExecution evaluateWithDiagnostics() { + checkExecutable(); + ExecutionDiagnostics diagnostics = new ExecutionDiagnostics(); + diagnostics.beginWork(); + try { + T result = evaluate(executor.withDiagnostics(diagnostics)); + return new QueryExecution<>(result, diagnostics::snapshot); + } finally { + diagnostics.endWork(); + } + } + + /** + * Evaluates with either the ordinary or a request-scoped diagnostic executor. + * + * @param execution query executor configured for this evaluation + * @return evaluated result owned by the caller + */ + protected abstract T evaluate(SparqlQueryExecutor execution); + @Override public String getQueryString() { return queryString; @@ -101,7 +142,7 @@ protected long timeoutMillis() { return Math.max(1L, millis); } - protected void checkExecutable() { + private void checkExecutable() { executionGuard.run(); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseBooleanQuery.java b/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseBooleanQuery.java index d3890c4c7..c516fe68f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseBooleanQuery.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseBooleanQuery.java @@ -1,46 +1,34 @@ package fr.inria.corese.core.next.query.impl.query; import fr.inria.corese.core.next.query.api.BooleanQuery; -import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; -import fr.inria.corese.core.next.query.impl.sparql.execution.NextSparqlPipelineExecutor; +import fr.inria.corese.core.next.query.impl.sparql.execution.SparqlQueryExecutor; import fr.inria.corese.core.next.query.impl.sparql.ast.AskQueryAst; -import java.util.Objects; - /** * Prepared SPARQL ASK query. * - *

    Delegates evaluation to {@link NextSparqlPipelineExecutor}.

    + *

    Delegates evaluation to {@link SparqlQueryExecutor}.

    */ public final class CoreseBooleanQuery extends AbstractCoreseQuery implements BooleanQuery { - private final NextSparqlPipelineExecutor executor; - private final AskQueryAst parsedQuery; - public CoreseBooleanQuery( String queryString, - NextSparqlPipelineExecutor executor, + SparqlQueryExecutor executor, Runnable executionGuard) { this(queryString, null, executor, executionGuard); } - /** Reuses the preparation AST while constructing fresh runtime state for each evaluation. */ + /** Retains immutable compilation separately from execution state for repeated evaluations. */ public CoreseBooleanQuery( String queryString, AskQueryAst parsedQuery, - NextSparqlPipelineExecutor executor, + SparqlQueryExecutor executor, Runnable executionGuard) { - super(queryString, executionGuard); - this.executor = Objects.requireNonNull(executor, "executor"); - this.parsedQuery = parsedQuery; + super(queryString, executionGuard, executor, parsedQuery); } @Override - public Boolean evaluate() throws QueryEvaluationException { - checkExecutable(); - if (parsedQuery == null) { - return executor.evaluateBoolean(getQueryString(), getBindings(), getDataset(), timeoutMillis()); - } - return executor.evaluateBoolean(parsedQuery, getBindings(), getDataset(), timeoutMillis()); + protected Boolean evaluate(SparqlQueryExecutor execution) { + return execution.evaluateBoolean(preparedPlan(), getBindings(), getDataset(), timeoutMillis()); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseGraphQuery.java b/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseGraphQuery.java index 9e5f874b6..8a107acab 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseGraphQuery.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseGraphQuery.java @@ -1,50 +1,38 @@ package fr.inria.corese.core.next.query.impl.query; import fr.inria.corese.core.next.query.api.GraphQuery; -import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; import fr.inria.corese.core.next.query.api.result.GraphQueryResult; -import fr.inria.corese.core.next.query.impl.sparql.execution.NextSparqlPipelineExecutor; +import fr.inria.corese.core.next.query.impl.sparql.execution.SparqlQueryExecutor; import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; -import java.util.Objects; - /** * Prepared SPARQL CONSTRUCT or DESCRIBE query. * - *

    Delegates evaluation to {@link NextSparqlPipelineExecutor}. Initial bindings, + *

    Delegates evaluation to {@link SparqlQueryExecutor}. Initial bindings, * dataset override, and timeout are propagated into the KGRAM evaluation layer * on each {@link #evaluate()} call. The CONSTRUCT template is applied to each * WHERE-clause result mapping to materialise the output statements.

    */ public final class CoreseGraphQuery extends AbstractCoreseQuery implements GraphQuery { - private final NextSparqlPipelineExecutor executor; - private final QueryAst parsedQuery; - public CoreseGraphQuery( String queryString, - NextSparqlPipelineExecutor executor, + SparqlQueryExecutor executor, Runnable executionGuard) { this(queryString, null, executor, executionGuard); } - /** Reuses the preparation AST while constructing fresh runtime state for each evaluation. */ + /** Retains immutable compilation separately from execution state for repeated evaluations. */ public CoreseGraphQuery( String queryString, QueryAst parsedQuery, - NextSparqlPipelineExecutor executor, + SparqlQueryExecutor executor, Runnable executionGuard) { - super(queryString, executionGuard); - this.executor = Objects.requireNonNull(executor, "executor"); - this.parsedQuery = parsedQuery; + super(queryString, executionGuard, executor, parsedQuery); } @Override - public GraphQueryResult evaluate() throws QueryEvaluationException { - checkExecutable(); - if (parsedQuery == null) { - return executor.evaluateGraph(getQueryString(), getBindings(), getDataset(), timeoutMillis()); - } - return executor.evaluateGraph(parsedQuery, getBindings(), getDataset(), timeoutMillis()); + protected GraphQueryResult evaluate(SparqlQueryExecutor execution) { + return execution.evaluateGraph(preparedPlan(), getBindings(), getDataset(), timeoutMillis()); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseTupleQuery.java b/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseTupleQuery.java index 2f80cc023..ce5536793 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseTupleQuery.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseTupleQuery.java @@ -1,49 +1,37 @@ package fr.inria.corese.core.next.query.impl.query; import fr.inria.corese.core.next.query.api.TupleQuery; -import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; import fr.inria.corese.core.next.query.api.result.TupleQueryResult; -import fr.inria.corese.core.next.query.impl.sparql.execution.NextSparqlPipelineExecutor; +import fr.inria.corese.core.next.query.impl.sparql.execution.SparqlQueryExecutor; import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst; -import java.util.Objects; - /** * Prepared SPARQL SELECT query. * - *

    Delegates evaluation to {@link NextSparqlPipelineExecutor}. Initial bindings, + *

    Delegates evaluation to {@link SparqlQueryExecutor}. Initial bindings, * dataset override, and timeout are propagated into the KGRAM evaluation layer * on each {@link #evaluate()} call.

    */ public final class CoreseTupleQuery extends AbstractCoreseQuery implements TupleQuery { - private final NextSparqlPipelineExecutor executor; - private final SelectQueryAst parsedQuery; - public CoreseTupleQuery( String queryString, - NextSparqlPipelineExecutor executor, + SparqlQueryExecutor executor, Runnable executionGuard) { this(queryString, null, executor, executionGuard); } - /** Reuses the preparation AST while constructing fresh runtime state for each evaluation. */ + /** Retains immutable compilation separately from execution state for repeated evaluations. */ public CoreseTupleQuery( String queryString, SelectQueryAst parsedQuery, - NextSparqlPipelineExecutor executor, + SparqlQueryExecutor executor, Runnable executionGuard) { - super(queryString, executionGuard); - this.executor = Objects.requireNonNull(executor, "executor"); - this.parsedQuery = parsedQuery; + super(queryString, executionGuard, executor, parsedQuery); } @Override - public TupleQueryResult evaluate() throws QueryEvaluationException { - checkExecutable(); - if (parsedQuery == null) { - return executor.evaluateTuple(getQueryString(), getBindings(), getDataset(), timeoutMillis()); - } - return executor.evaluateTuple(parsedQuery, getBindings(), getDataset(), timeoutMillis()); + protected TupleQueryResult evaluate(SparqlQueryExecutor execution) { + return execution.evaluateTuple(preparedPlan(), getBindings(), getDataset(), timeoutMillis()); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseUpdate.java b/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseUpdate.java index b3060bf4a..c8eea6900 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseUpdate.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseUpdate.java @@ -4,7 +4,7 @@ import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; import fr.inria.corese.core.next.query.impl.sparql.ast.UpdateRequestAst; import fr.inria.corese.core.next.query.impl.sparql.parser.SparqlParser; -import fr.inria.corese.core.next.query.impl.sparql.update.NextSparqlUpdateExecutor; +import fr.inria.corese.core.next.query.impl.sparql.update.SparqlUpdateExecutor; import fr.inria.corese.core.next.query.impl.sparql.update.UpdateTransaction; import fr.inria.corese.core.next.storage.api.StorageManager; @@ -62,7 +62,7 @@ public CoreseUpdate(String updateString, StorageManager storage, SparqlParser pa public void execute() throws QueryEvaluationException { executionGuard.run(); UpdateRequestAst request = (UpdateRequestAst) parser.parse(updateString); - NextSparqlUpdateExecutor executor = new NextSparqlUpdateExecutor(storage, request.prologue()); + SparqlUpdateExecutor executor = new SparqlUpdateExecutor(storage, request.prologue()); transaction.accept(() -> executor.execute(request)); } } 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 55177a450..95441b9ff 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 @@ -34,7 +34,7 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst; import fr.inria.corese.core.next.query.impl.sparql.ast.UpdateRequestAst; -import fr.inria.corese.core.next.query.impl.sparql.execution.NextSparqlPipelineExecutor; +import fr.inria.corese.core.next.query.impl.sparql.execution.SparqlQueryExecutor; import fr.inria.corese.core.next.query.impl.sparql.update.UpdateTransaction; import fr.inria.corese.core.next.storage.api.StorageManager; import fr.inria.corese.core.next.storage.api.exception.StorageException; @@ -66,7 +66,7 @@ public final class CoreseRepositoryConnection implements RepositoryConnection { private final Repository repository; private final StorageManager storage; - private final NextSparqlPipelineExecutor executor; + private final SparqlQueryExecutor executor; private final SparqlParser parser; private Dataset connectionDataset; private Transaction transaction; @@ -75,7 +75,7 @@ public final class CoreseRepositoryConnection implements RepositoryConnection { CoreseRepositoryConnection(Repository repository, StorageManager storage) { this.repository = repository; this.storage = storage; - this.executor = new NextSparqlPipelineExecutor(storage); + this.executor = new SparqlQueryExecutor(storage); this.parser = new SparqlParser(); } @@ -499,8 +499,8 @@ private QueryAst parse(String queryString) throws QuerySyntaxException { return parser.parse(queryString); } - private NextSparqlPipelineExecutor executorFor(String baseIRI) { - return baseIRI == null ? executor : new NextSparqlPipelineExecutor(storage, baseIRI); + private SparqlQueryExecutor executorFor(String baseIRI) { + return baseIRI == null ? executor : new SparqlQueryExecutor(storage, baseIRI); } private String requireSource(String source, String parameterName) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/repository/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/repository/package-info.java index 50ab17c88..6a3d6f170 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/repository/package-info.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/repository/package-info.java @@ -7,7 +7,7 @@ * *

    The parser, AST, bridge, and KGRAM engine are invisible from this package. * {@link fr.inria.corese.core.next.query.impl.repository.CoreseRepositoryConnection} - * hides {@link fr.inria.corese.core.next.query.impl.sparql.execution.NextSparqlPipelineExecutor} + * hides {@link fr.inria.corese.core.next.query.impl.sparql.execution.SparqlQueryExecutor} * and validates query syntax at preparation time before returning typed query objects.

    */ package fr.inria.corese.core.next.query.impl.repository; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AddRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AddRequestAst.java index 9a8883f9b..e5490dd3c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AddRequestAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AddRequestAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Represents the ADD operation as defined in the SPARQL 1.1 recommendation. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AggregateAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AggregateAst.java index a3d81c1c9..9889c2010 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AggregateAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AggregateAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; public record AggregateAst( AggregateFunction function, diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java index 1caec31cc..64f980c66 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/BgpAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/BgpAst.java index df0cf7e97..f7f1637c6 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/BgpAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/BgpAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/BindAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/BindAst.java index f2f9d52bb..82c157d1d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/BindAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/BindAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * BIND(expression AS ?var) clause in SPARQL 1.1 diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ClearRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ClearRequestAst.java index 4d4d018e0..9c312c634 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ClearRequestAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ClearRequestAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Represents the CLEAR operation as defined in the SPARQL 1.1 recommendation. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java index 8e8a46e5d..0736c8d1f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructTemplateAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructTemplateAst.java index c4da89f21..a1733d77f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructTemplateAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructTemplateAst.java @@ -2,8 +2,8 @@ import java.util.List; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; /** * CONSTRUCT template: list of triple templates used to build the output RDF diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/CopyRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/CopyRequestAst.java index 7b447e330..426f24c22 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/CopyRequestAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/CopyRequestAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Represents the COPY operation as defined in the SPARQL 1.1 recommendation. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/CreateRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/CreateRequestAst.java index 832d56ff0..32da19dff 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/CreateRequestAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/CreateRequestAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Represents the CREATE operation as defined diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DatasetClauseAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DatasetClauseAst.java index 9d9004b39..174743ac3 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DatasetClauseAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DatasetClauseAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; import java.util.LinkedHashSet; import java.util.Set; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteDataRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteDataRequestAst.java index a0bfb52a8..fa2e7e768 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteDataRequestAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteDataRequestAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Represents the {@code DELETE DATA} operation as defined in the diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteWhereRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteWhereRequestAst.java index 24b72d056..837f4fb7a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteWhereRequestAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DeleteWhereRequestAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Represents the {@code DELETE WHERE} operation as defined in the diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java index efb7d12e2..ed92a8071 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DropRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DropRequestAst.java index e4a7e39b6..26679db99 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DropRequestAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DropRequestAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Represents the DROP operation as defined in the SPARQL 1.1 recommendation. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/FilterAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/FilterAst.java index 8845ec56e..94e20846a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/FilterAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/FilterAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Filter clause in a SPARQL query diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GraphAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GraphAst.java index 05e810c3b..5969515fd 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GraphAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GraphAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** A named graph pattern, with an IRI or variable graph name. */ public record GraphAst(TermAst name, GroupGraphPatternAst pattern) implements PatternAst { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GraphRefAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GraphRefAst.java index a15da6a30..5b5daff65 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GraphRefAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GraphRefAst.java @@ -2,8 +2,8 @@ import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; public record GraphRefAst(IriAst graph, boolean named, boolean all, boolean defaultGraph) implements VisitableAst { public GraphRefAst { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GroupByAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GroupByAst.java index e6c3f313d..a6da61ac3 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GroupByAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GroupByAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; import java.util.List; import java.util.Map; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GroupGraphPatternAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GroupGraphPatternAst.java index 7f06bcc24..9a07d468c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GroupGraphPatternAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/GroupGraphPatternAst.java @@ -3,7 +3,7 @@ import java.util.List; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Group graph pattern: { pattern1 pattern2 ... } (WHERE clause content). diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/HavingAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/HavingAst.java index bf7404422..738070f82 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/HavingAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/HavingAst.java @@ -2,8 +2,8 @@ import java.util.List; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; /** * SPARQL {@code HAVING} clause: boolean constraints evaluated after grouping. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/InsertDataRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/InsertDataRequestAst.java index 8b8f25b50..b31302092 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/InsertDataRequestAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/InsertDataRequestAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Represents the {@code INSERT DATA} operation as defined in the diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/IriAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/IriAst.java index 8e6c310f4..e79f55596 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/IriAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/IriAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * IRI or QName in a triple pattern (e.g. <http://...>, foaf:Person, a). diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/LiteralAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/LiteralAst.java index 32ca69d64..f3f34cd38 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/LiteralAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/LiteralAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * RDF literal in a triple pattern (lexical form, optional language tag or datatype). diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/LoadRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/LoadRequestAst.java index 434082e9f..6c4ab38ef 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/LoadRequestAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/LoadRequestAst.java @@ -2,7 +2,7 @@ import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Represents the LOAD operation as defined in the SPARQL 1.1 recommendation. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/MinusAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/MinusAst.java index 2cabc156b..8478f053b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/MinusAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/MinusAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * AST node representing a SPARQL {@code MINUS} graph pattern. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ModifyRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ModifyRequestAst.java index f1fb79970..ea41851a2 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ModifyRequestAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ModifyRequestAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * DELETE/INSERT templates evaluated against one snapshot of the WHERE solutions. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/MoveRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/MoveRequestAst.java index dd8ab37d0..d26e6e946 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/MoveRequestAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/MoveRequestAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Represents the MOVE operation as defined in the SPARQL 1.1 recommendation. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/NamedGraphQuadsAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/NamedGraphQuadsAst.java index 7a3f192ec..1712ba50b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/NamedGraphQuadsAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/NamedGraphQuadsAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/OptionalAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/OptionalAst.java index 8029b3f19..d038a77de 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/OptionalAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/OptionalAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Optional can contain BGP, FILTER, UNION diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/OrderConditionAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/OrderConditionAst.java index 435a0d634..2713477ca 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/OrderConditionAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/OrderConditionAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; /** * One ORDER BY condition. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PatternAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PatternAst.java index 4d6a99bd8..d658604f9 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PatternAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PatternAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; /** * Element of a group graph pattern (BGP, optional, union, etc.). diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java index 45e621475..388805e9f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; import java.util.Objects; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ProjectionAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ProjectionAst.java index 81ca236bb..090ef6194 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ProjectionAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ProjectionAst.java @@ -4,8 +4,8 @@ import java.util.Map; import java.util.Set; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; /** * SPARQL SELECT projection: either {@code SELECT *} (all variables from the diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QuadsAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QuadsAst.java index f94c828da..e8baf79b6 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QuadsAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QuadsAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java index d664dd08e..1bfc04caf 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; /** * Root interface for an abstract syntax trees for operation diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java index 01bb43103..e8fe372e0 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java @@ -3,8 +3,8 @@ import fr.inria.corese.core.next.data.spi.term.IRIUtils; import fr.inria.corese.core.next.data.spi.io.IOConstants; import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java index b9062a99f..b3e4f84e7 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ServiceAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ServiceAst.java index a14083cd8..715969793 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ServiceAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ServiceAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * AST node representing a SPARQL {@code SERVICE} graph pattern. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SolutionModifierAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SolutionModifierAst.java index 35ca945b0..29efaca3a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SolutionModifierAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SolutionModifierAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SubQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SubQueryAst.java index 3d3218113..1e78d95c7 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SubQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SubQueryAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; public record SubQueryAst(QueryAst query) implements PatternAst { public SubQueryAst { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TermAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TermAst.java index 023ac6a76..54bc99676 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TermAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TermAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; /** * AST node for a SPARQL term (variable, IRI or literal) in a triple pattern. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java index 2ee0f7972..03ed06a49 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/TriplePatternAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; import fr.inria.corese.core.next.query.impl.sparql.ast.path.PredicatePathAst; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UnionAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UnionAst.java index bffabce95..a59730f52 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UnionAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UnionAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * AST node representing a SPARQL {@code UNION} of two graph patterns. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestAst.java index aab47810d..d89cafea4 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestAst.java @@ -2,7 +2,7 @@ import java.util.List; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Represents ordered update operations and their effective PREFIX/BASE snapshots. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestUnitAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestUnitAst.java index a74002551..6743ba089 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestUnitAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestUnitAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; /** * Root interface for all operations related to the SPARQL Update operations listed in SPARQL 1.1 Update. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValueMappingAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValueMappingAst.java index d85a7a3d1..c82b8df7f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValueMappingAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValueMappingAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; import java.util.Collections; import java.util.LinkedHashMap; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValuesAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValuesAst.java index c8cdf3c06..11787a377 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValuesAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ValuesAst.java @@ -4,7 +4,7 @@ import java.util.List; import java.util.Set; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * SPARQL {@code VALUES} table. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/VarAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/VarAst.java index c0637fcb9..a25724763 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/VarAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/VarAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Variable in a triple pattern (e.g. ?s, $x). diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractBinaryConstraintAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractBinaryConstraintAst.java index d3369e6dd..8acf9c960 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractBinaryConstraintAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractBinaryConstraintAst.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.Objects; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; /** @@ -23,7 +23,7 @@ public TermAst getLeftArgument() { return this.leftArgument; } - protected void setLeftArgument(TermAst arg) { + protected final void setLeftArgument(TermAst arg) { this.leftArgument = arg; } @@ -32,7 +32,7 @@ public TermAst getRightArgument() { return this.rightArgument; } - protected void setRightArgument(TermAst arg) { + protected final void setRightArgument(TermAst arg) { this.rightArgument = arg; } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractChainableOperatorAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractChainableOperatorAst.java index 750d017c2..b61b9b2bc 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractChainableOperatorAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractChainableOperatorAst.java @@ -4,20 +4,20 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; import java.util.List; +import java.util.function.Function; public abstract class AbstractChainableOperatorAst extends AbstractBinaryConstraintAst { - protected AbstractChainableOperatorAst(List args) { + protected AbstractChainableOperatorAst(List args, Function, TermAst> factory) { super(args); this.setRightArgument(args.getLast()); if(args.size() == 2) { this.setLeftArgument(args.getFirst()); } else if(args.size() > 2) { List tail = args.subList(0, args.size() - 1); - this.setLeftArgument(this.create(tail)); + this.setLeftArgument(factory.apply(tail)); } else { throw new QuerySyntaxException("Unexpected number of arguments (" + args.size() + ") for binary chainable operator"); } } - protected abstract TermAst create(List args); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractUnaryConstraintAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractUnaryConstraintAst.java index 790ff1498..64d6f03df 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractUnaryConstraintAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractUnaryConstraintAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractUnlimitedArgumentsFunctionAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractUnlimitedArgumentsFunctionAst.java index becb5ae86..6bbafab94 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractUnlimitedArgumentsFunctionAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AbstractUnlimitedArgumentsFunctionAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AddAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AddAst.java index 67e893d51..bd69a7a17 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AddAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AddAst.java @@ -10,13 +10,9 @@ */ public class AddAst extends AbstractChainableOperatorAst implements NumericExpressionAst { public AddAst(List args) { - super(args); + super(args, AddAst::new); } - @Override - protected TermAst create(List args) { - return new AddAst(args); - } @Override public String getName() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AndAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AndAst.java index 4849f6071..9d991d14e 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AndAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/AndAst.java @@ -9,13 +9,9 @@ */ public class AndAst extends AbstractChainableOperatorAst implements BooleanExpressionAst { public AndAst(List args) { - super(args); + super(args, AndAst::new); } - @Override - protected TermAst create(List args) { - return new AndAst(args); - } @Override public String getName() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/BnodeAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/BnodeAst.java index 5bbb12e75..d25c100dd 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/BnodeAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/BnodeAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.ExprAst; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/DifferentAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/DifferentAst.java index c4c606131..a7bb8c440 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/DifferentAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/DifferentAst.java @@ -9,13 +9,9 @@ */ public class DifferentAst extends AbstractChainableOperatorAst implements BooleanExpressionAst { public DifferentAst(List args) { - super(args); + super(args, DifferentAst::new); } - @Override - protected TermAst create(List args) { - return new DifferentAst(args); - } @Override public String getName() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/DivideAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/DivideAst.java index 8442fb300..8f55892db 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/DivideAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/DivideAst.java @@ -9,13 +9,9 @@ */ public class DivideAst extends AbstractChainableOperatorAst implements NumericExpressionAst { public DivideAst(List args) { - super(args); + super(args, DivideAst::new); } - @Override - protected TermAst create(List args) { - return new DivideAst(args); - } @Override public String getName() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/EqualsAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/EqualsAst.java index ae9f28fc5..ad4cd61fc 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/EqualsAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/EqualsAst.java @@ -9,13 +9,9 @@ */ public class EqualsAst extends AbstractChainableOperatorAst implements BooleanExpressionAst { public EqualsAst(List args) { - super(args); + super(args, EqualsAst::new); } - @Override - protected TermAst create(List args) { - return new EqualsAst(args); - } @Override public String getName() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/ExistsAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/ExistsAst.java index 7a6ff1a38..9eb1d6c4c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/ExistsAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/ExistsAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst; import java.util.Objects; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/FunctionCallAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/FunctionCallAst.java index bcdc6a348..e4079acba 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/FunctionCallAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/FunctionCallAst.java @@ -2,7 +2,7 @@ import java.util.List; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.ExprAst; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/GreaterOrEqualThanAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/GreaterOrEqualThanAst.java index 042425d0a..4fafa714b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/GreaterOrEqualThanAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/GreaterOrEqualThanAst.java @@ -9,13 +9,9 @@ */ public class GreaterOrEqualThanAst extends AbstractChainableOperatorAst implements BooleanExpressionAst { public GreaterOrEqualThanAst(List args) { - super(args); + super(args, GreaterOrEqualThanAst::new); } - @Override - protected TermAst create(List args) { - return new GreaterOrEqualThanAst(args); - } @Override public String getName() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/GreaterThanAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/GreaterThanAst.java index 59b1c0d98..eaa97ba9b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/GreaterThanAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/GreaterThanAst.java @@ -9,13 +9,9 @@ */ public class GreaterThanAst extends AbstractChainableOperatorAst implements BooleanExpressionAst { public GreaterThanAst(List args) { - super(args); + super(args, GreaterThanAst::new); } - @Override - protected TermAst create(List args) { - return new GreaterThanAst(args); - } @Override public String getName() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/IfAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/IfAst.java index 63c44cc0b..df4e92a1b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/IfAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/IfAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.ConstraintAst; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/InAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/InAst.java index 47c12032c..567b365c8 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/InAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/InAst.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.Objects; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; /** diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/LowerOrEqualThanAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/LowerOrEqualThanAst.java index d300e6c2b..340c0d1e5 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/LowerOrEqualThanAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/LowerOrEqualThanAst.java @@ -9,13 +9,9 @@ */ public class LowerOrEqualThanAst extends AbstractChainableOperatorAst implements BooleanExpressionAst { public LowerOrEqualThanAst(List args) { - super(args); + super(args, LowerOrEqualThanAst::new); } - @Override - protected TermAst create(List args) { - return new LowerOrEqualThanAst(args); - } @Override public String getName() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/LowerThanAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/LowerThanAst.java index b21ca974e..7c5a40734 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/LowerThanAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/LowerThanAst.java @@ -9,13 +9,9 @@ */ public class LowerThanAst extends AbstractChainableOperatorAst implements BooleanExpressionAst { public LowerThanAst(List args) { - super(args); + super(args, LowerThanAst::new); } - @Override - protected TermAst create(List args) { - return new LowerThanAst(args); - } @Override public String getName() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/MultiplyAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/MultiplyAst.java index c2a8e31b1..073f3d628 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/MultiplyAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/MultiplyAst.java @@ -9,13 +9,9 @@ */ public class MultiplyAst extends AbstractChainableOperatorAst implements NumericExpressionAst { public MultiplyAst(List args) { - super(args); + super(args, MultiplyAst::new); } - @Override - protected TermAst create(List args) { - return new MultiplyAst(args); - } @Override public String getName() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/NotExistsAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/NotExistsAst.java index 72cff273f..385f7a04a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/NotExistsAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/NotExistsAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst; import java.util.Objects; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/NotInAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/NotInAst.java index 1f9e40212..e45169e86 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/NotInAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/NotInAst.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.Objects; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; /** diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/NowAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/NowAst.java index d8b22964f..5449a050d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/NowAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/NowAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Function {@code NOW()} in SPARQL 1.1 — §17.4.5.1. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/OrAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/OrAst.java index 9334e13a6..475afec6a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/OrAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/OrAst.java @@ -9,13 +9,9 @@ */ public class OrAst extends AbstractChainableOperatorAst implements BooleanExpressionAst { public OrAst(List args) { - super(args); + super(args, OrAst::new); } - @Override - protected TermAst create(List args) { - return new OrAst(args); - } @Override public String getName() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/RandAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/RandAst.java index 45f997558..7f22461f3 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/RandAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/RandAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Function {@code RAND()} in SPARQL 1.1. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/ReplaceAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/ReplaceAst.java index 34f45fbba..e11aedf8c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/ReplaceAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/ReplaceAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/StrUuidAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/StrUuidAst.java index b51663084..9582d0e1b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/StrUuidAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/StrUuidAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Function {@code STRUUID()} in SPARQL 1.1 diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/SubstrAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/SubstrAst.java index d3a77f70c..28192310d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/SubstrAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/SubstrAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/SubtractAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/SubtractAst.java index 94c42a206..3dbff120c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/SubtractAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/SubtractAst.java @@ -10,13 +10,9 @@ */ public class SubtractAst extends AbstractChainableOperatorAst implements NumericExpressionAst { public SubtractAst(List args) { - super(args); + super(args, SubtractAst::new); } - @Override - protected TermAst create(List args) { - return new SubtractAst(args); - } @Override public String getName() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/TrinaryRegexAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/TrinaryRegexAst.java index dc9f7053e..aaecddba7 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/TrinaryRegexAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/TrinaryRegexAst.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; import java.util.List; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/UuidAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/UuidAst.java index 651f191a5..ce8660d9f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/UuidAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/UuidAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; /** * Function {@code UUID()} in SPARQL 1.1 diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.java index 687aa40c8..769d90662 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/AlternativePathAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; public record AlternativePathAst(PathAst left, PathAst right) implements PathAst { public AlternativePathAst { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java index c0e7f3230..7a09400d6 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/InversePathAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; public record InversePathAst(PathAst pathAst) implements PathAst { public InversePathAst { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.java index 161bd768f..b2fe41537 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/NegatedPropertySetPathAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import java.util.List; import java.util.Objects; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java index 1fdd8bcbf..3eaff7cee 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OneOrMorePathAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; public record OneOrMorePathAst(PathAst pathAst) implements PathAst { public OneOrMorePathAst { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java index 4b93ffedb..a7ba2d6e5 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/OptionalPathAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; public record OptionalPathAst(PathAst pathAst) implements PathAst { public OptionalPathAst { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java index 8ccd64049..6e5246ba5 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PathAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VisitableAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.VisitableAst; public sealed interface PathAst extends VisitableAst diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java index 5f68ad6f5..ce7ce86be 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/PredicatePathAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; public record PredicatePathAst(TermAst predicate) implements PathAst { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.java index c28d7eeff..00cfa6587 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/SequencePathAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; public record SequencePathAst(PathAst left, PathAst right) implements PathAst { public SequencePathAst { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java index ea0b2407b..a50dcabc4 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/path/ZeroOrMorePathAst.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.path; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AstVisitor; public record ZeroOrMorePathAst(PathAst pathAst) implements PathAst { public ZeroOrMorePathAst { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/support/AbstractAstVisitor.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/visitor/AbstractAstVisitor.java similarity index 95% rename from src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/support/AbstractAstVisitor.java rename to src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/visitor/AbstractAstVisitor.java index 75341c0be..68d0deaea 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/support/AbstractAstVisitor.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/visitor/AbstractAstVisitor.java @@ -1,4 +1,4 @@ -package fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support; +package fr.inria.corese.core.next.query.impl.sparql.ast.visitor; import fr.inria.corese.core.next.query.impl.sparql.ast.*; import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/support/AstVisitor.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/visitor/AstVisitor.java similarity index 93% rename from src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/support/AstVisitor.java rename to src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/visitor/AstVisitor.java index bf00d5d44..914b8ec45 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/support/AstVisitor.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/visitor/AstVisitor.java @@ -1,4 +1,4 @@ -package fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support; +package fr.inria.corese.core.next.query.impl.sparql.ast.visitor; import fr.inria.corese.core.next.query.impl.sparql.ast.*; import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/visitor/VisitableAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/visitor/VisitableAst.java new file mode 100644 index 000000000..327e02253 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/visitor/VisitableAst.java @@ -0,0 +1,6 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast.visitor; + +public interface VisitableAst { + + void accept(AstVisitor visitor); +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/visitor/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/visitor/package-info.java new file mode 100644 index 000000000..fb3d204a4 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/visitor/package-info.java @@ -0,0 +1,5 @@ +/** + * Traversal contracts owned by the SPARQL syntax tree. Semantic validation and + * compilation depend on these contracts; syntax tree types do not depend on the parser. + */ +package fr.inria.corese.core.next.query.impl.sparql.ast.visitor; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java index f8504c1eb..54ec2287b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdge.java @@ -1,5 +1,8 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopy; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopyable; + import fr.inria.corese.core.next.query.impl.engine.model.Edge; import fr.inria.corese.core.next.query.impl.engine.model.Node; import fr.inria.corese.core.next.query.impl.engine.model.KgramNodes; @@ -10,7 +13,7 @@ * A query-side {@link Edge} produced by the WHERE compiler from a * {@link fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAst}. */ -final class AstBackedEdge implements Edge { +final class AstBackedEdge implements Edge, PlanCopyable { private final Node subject; private final Node edgeNode; @@ -36,6 +39,15 @@ final class AstBackedEdge implements Edge { this.graph = graph; } + /** Copies the query edge and its indexed nodes. */ + @Override + public AstBackedEdge copyPlan(PlanCopy copies) { + AstBackedEdge target = new AstBackedEdge(copies.copy(subject), copies.copy(getProperty()), + copies.copy(object), copies.copy(graph)); + copies.remember(this, target); + return target; + } + @Override public Node getNode(int i) { return switch (i) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java index 4328acfff..db79bfde5 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java @@ -1,5 +1,9 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopy; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopyable; + +import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; import fr.inria.corese.core.next.data.api.model.DatatypeValue; import fr.inria.corese.core.next.query.impl.engine.model.BindingContext; @@ -25,22 +29,20 @@ import java.util.Set; /** Immutable KGRAM expression backed directly by a Corese-next AST term. */ -public final class AstBackedExpr implements Expr { +public final class AstBackedExpr implements Expr, PlanCopyable { private final TermAst source; private final WhereCompiler whereCompiler; /** * Snapshot of the enclosing group's in-scope variables, captured at filter - * creation time so that {@link NextFilterFromAst#getVariables()} reports the + * creation time so that {@link AstBackedFilter#getVariables()} reports the * correct scheduling dependencies even after {@code WhereCompiler} has moved * to a different scope (e.g., after returning from a MINUS body). */ private final Set capturedInScope; - private final NextFilterFromAst filterView; + private final AstBackedFilter filterView; private int index = ExprType.UNBOUND; - private int subtype = ExprType.GLOBAL; - private int operator; - private boolean publicExpression; + private final int operator; public AstBackedExpr(TermAst source) { this(source, null); @@ -53,7 +55,25 @@ public AstBackedExpr(TermAst source) { ? Set.copyOf(whereCompiler.inScopeVariables()) : Set.of(); this.operator = operator(source); - this.filterView = new NextFilterFromAst(this); + this.filterView = new AstBackedFilter(this); + } + + // A filter points to its compiler, whose pattern cache can point back to this filter. + @SuppressWarnings("this-escape") // The registered shell is only linked by the copy operation, never evaluated. + private AstBackedExpr(AstBackedExpr original, PlanCopy copies) { + source = original.source; + capturedInScope = original.capturedInScope; + operator = original.operator; + index = original.index; + filterView = new AstBackedFilter(this); + copies.remember(original, this); + whereCompiler = copies.copy(original.whereCompiler); + } + + /** Copies expression indexes and compiler caches while sharing immutable syntax. */ + @Override + public AstBackedExpr copyPlan(PlanCopy copies) { + return new AstBackedExpr(this, copies); } public Optional sourceAst() { @@ -75,7 +95,7 @@ public Filter getFilter() { } @Override - public Object getPattern() { + public Exp getPattern() { return switch (source) { case ExistsAst(var pattern) -> whereCompiler == null ? null : whereCompiler.compile(pattern); @@ -83,47 +103,13 @@ case ExistsAst(var pattern) -> }; } - @Override - public boolean isSystem() { - return false; - } - - @Override - public boolean isPublic() { - return publicExpression; - } - - @Override - public void setPublic(boolean value) { - publicExpression = value; - } - - @Override - public boolean isDynamic() { - return false; - } - - @Override - public boolean isTrace() { - return false; - } - - @Override - public boolean isDebug() { - return false; - } - @Override public String getLabel() { return source.getName(); } @Override - public String getModality() { - return null; - } - - @Override + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. public List getExpList() { List expressions = new ArrayList<>(); for (TermAst child : children(source)) { @@ -137,21 +123,6 @@ public Expr getExp(int childIndex) { return getExpList().get(childIndex); } - @Override - public void setExp(int childIndex, Expr expression) { - throw new UnsupportedOperationException("Corese-next AST expressions are immutable"); - } - - @Override - public Expr getArg() { - return getExpList().isEmpty() ? null : getExpList().getFirst(); - } - - @Override - public void setArg(Expr expression) { - throw new UnsupportedOperationException("Corese-next AST expressions are immutable"); - } - @Override public DatatypeValue getValue() { if (source instanceof IriAst || source instanceof LiteralAst) { @@ -163,11 +134,6 @@ public DatatypeValue getValue() { return null; } - @Override - public DatatypeValue getDatatypeValue() { - return getValue(); - } - @Override public int type() { return switch (source) { @@ -181,12 +147,7 @@ public int type() { @Override public int subtype() { - return subtype; - } - - @Override - public void setSubtype(int value) { - subtype = value; + return ExprType.GLOBAL; } @Override @@ -199,11 +160,6 @@ public boolean match(int value) { return operator == value; } - @Override - public void setOper(int value) { - operator = value; - } - @Override public boolean isAggregate() { return source instanceof AggregateAst; @@ -214,11 +170,6 @@ public boolean isRecAggregate() { return new VariableScopeAnalyzer().containsAggregate(source); } - @Override - public boolean isExist() { - return source instanceof ExistsAst || source instanceof NotExistsAst; - } - @Override public boolean isRecExist() { return contains(ExistsAst.class) || contains(NotExistsAst.class); @@ -234,21 +185,6 @@ public boolean isConstant() { return source instanceof IriAst || source instanceof LiteralAst; } - @Override - public boolean isFuncall() { - return source instanceof FunctionCallAst; - } - - @Override - public boolean isBound() { - return source instanceof BoundAst; - } - - @Override - public boolean isDistinct() { - return source instanceof AggregateAst aggregate && aggregate.distinct(); - } - @Override public int arity() { return children(source).size(); @@ -264,41 +200,6 @@ public void setIndex(int value) { index = value; } - @Override - public Expr getDefine() { - return null; - } - - @Override - public void setDefine(Expr expression) { - throw new UnsupportedOperationException("Corese-next AST expressions are immutable"); - } - - @Override - public Expr getFunction() { - return null; - } - - @Override - public Expr getBody() { - return null; - } - - @Override - public Expr getVariable() { - return isVariable() ? this : null; - } - - @Override - public Expr getDefinition() { - return null; - } - - @Override - public boolean hasMetadata(String name) { - return false; - } - @Override public DatatypeValue evalWE( Evaluator evaluator, diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextFilterFromAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedFilter.java similarity index 80% rename from src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextFilterFromAst.java rename to src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedFilter.java index 031e15528..3b9b93b65 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextFilterFromAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedFilter.java @@ -1,10 +1,13 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopy; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopyable; + import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; import fr.inria.corese.core.next.query.impl.engine.model.Expr; import fr.inria.corese.core.next.query.impl.engine.model.Filter; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BoundAst; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AbstractAstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AbstractAstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; import java.util.LinkedHashSet; @@ -13,14 +16,22 @@ import java.util.Set; /** Filter view exposing native AST metadata through the Corese-next expression API. */ -public final class NextFilterFromAst implements Filter { +public final class AstBackedFilter implements Filter, PlanCopyable { private final AstBackedExpr owner; - NextFilterFromAst(AstBackedExpr owner) { + AstBackedFilter(AstBackedExpr owner) { this.owner = owner; } + /** Copies the owner expression and reuses its unique filter view within the new graph. */ + @Override + public AstBackedFilter copyPlan(PlanCopy copies) { + AstBackedFilter target = (AstBackedFilter) copies.copy(owner).getFilter(); + copies.remember(this, target); + return target; + } + @Override public List getVariables() { // Scheduling dependencies include variables inside EXISTS patterns (including diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java index a5d718e2d..a016255c2 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java @@ -239,6 +239,7 @@ private void applyDataset(Query query, DatasetClauseAst datasetClause, WhereComp !datasetClause.graphs().isEmpty() || !datasetClause.namedGraphs().isEmpty()); } + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. private List toNodeList(Iterable iris, WhereCompiler compiler) { List nodes = new ArrayList<>(); for (IriAst iri : iris) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeEvaluationContext.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeEvaluationContext.java index 34aeed95e..cc353c5bc 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeEvaluationContext.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeEvaluationContext.java @@ -17,7 +17,6 @@ import fr.inria.corese.core.next.query.impl.engine.spi.Environment; import fr.inria.corese.core.next.query.impl.engine.spi.Evaluator; import fr.inria.corese.core.next.query.impl.engine.spi.Producer; -import fr.inria.corese.core.next.query.impl.engine.eval.SparqlException; import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; @@ -135,12 +134,8 @@ boolean exists(GroupGraphPatternAst pattern) { if (environment == null || environment.getEval() == null || whereCompiler == null) { throw new QueryEvaluationException("EXISTS requires an active query evaluation context"); } - try { - return environment.getEval().exists( - producer, environment.getGraphNode(), environment, whereCompiler.compile(pattern)); - } catch (SparqlException exception) { - throw new QueryEvaluationException("Failed to evaluate EXISTS graph pattern", exception); - } + return environment.getEval().exists( + producer, environment.getGraphNode(), environment, whereCompiler.compile(pattern)); } DatatypeValue blankNode(TermAst label) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SolutionModifierCompiler.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SolutionModifierCompiler.java index 5a1bfbfb2..2773e503b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SolutionModifierCompiler.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SolutionModifierCompiler.java @@ -284,6 +284,7 @@ private static Node createSyntheticOrderNode(int syntheticIndex) { return NodeImpl.forVariable("__order_by_" + syntheticIndex); } + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. private static List toNodeExpressions(List nodes) { List expressions = new ArrayList<>(); for (Node node : nodes) { @@ -292,6 +293,7 @@ private static List toNodeExpressions(List nodes) { return expressions; } + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. private static List selectNodeList(List selectExpressions) { LinkedHashSet selectNodes = new LinkedHashSet<>(); for (Exp selectExpression : selectExpressions) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolver.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolver.java index 6b5fc6099..eeef1e5a7 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolver.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolver.java @@ -1,5 +1,8 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopy; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopyable; + import fr.inria.corese.core.next.common.text.RdfText; import fr.inria.corese.core.next.data.api.vocabulary.RDF; import fr.inria.corese.core.next.data.impl.namespace.PrefixHandler; @@ -21,7 +24,7 @@ import java.util.Objects; /** Resolves SPARQL terms against one immutable query-prologue snapshot. */ -public final class SparqlTermResolver { +public final class SparqlTermResolver implements PlanCopyable { private final PrefixHandler prefixes; private final String baseIri; @@ -37,6 +40,21 @@ public SparqlTermResolver(QueryPrologueAst prologue) { this.baseIri = effectivePrologue.baseIri().raw(); } + private SparqlTermResolver(SparqlTermResolver original, PlanCopy copies) { + // Prefixes are private, fully initialized in the public constructor and never mutated afterward. + prefixes = original.prefixes; + baseIri = original.baseIri; + original.variables.forEach((name, node) -> variables.put(name, copies.copy(node))); + } + + /** Copies variable-node interning without re-resolving constants or prologues. */ + @Override + public SparqlTermResolver copyPlan(PlanCopy copies) { + SparqlTermResolver target = new SparqlTermResolver(this, copies); + copies.remember(this, target); + return target; + } + public String resolveIri(String raw) { if (raw == null) { return null; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java index 85f4cf209..4e8760d1d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java @@ -1,6 +1,9 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopy; +import fr.inria.corese.core.next.query.impl.engine.pattern.PlanCopyable; + import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; import fr.inria.corese.core.next.query.impl.sparql.ast.BgpAst; import fr.inria.corese.core.next.query.impl.sparql.ast.BindAst; @@ -47,7 +50,7 @@ * {@link PatternAst} nodes — into a KGRAM {@link Exp} body that the engine can * evaluate. */ -public final class WhereCompiler { +public final class WhereCompiler implements PlanCopyable { private final SparqlTermResolver termResolver; private final Map compiledPatternCache = @@ -62,6 +65,23 @@ private WhereCompiler(QueryPrologueAst prologue) { termResolver = new SparqlTermResolver(prologue); } + @SuppressWarnings("this-escape") // Registering first preserves compiler/filter/cache cycles; the shell never executes. + private WhereCompiler(WhereCompiler original, PlanCopy copies) { + copies.remember(original, this); + termResolver = copies.copy(original.termResolver); + inScopeVariables = original.inScopeVariables; + synchronized (original.compiledPatternCache) { + original.compiledPatternCache.forEach((ast, pattern) -> + compiledPatternCache.put(ast, copies.copy(pattern))); + } + } + + /** Copies precompiled EXISTS patterns and the resolver into one execution graph. */ + @Override + public WhereCompiler copyPlan(PlanCopy copies) { + return new WhereCompiler(this, copies); + } + WhereCompiler withPrologue(QueryPrologueAst prologue) { return new WhereCompiler(prologue); } @@ -246,6 +266,7 @@ private Exp compileBind(BindAst bind) { } /** Lowers a SPARQL inline-data table to the runtime's native VALUES expression. */ + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. Exp compileValues(ValuesAst values) { List variables = new ArrayList<>(); for (VarAst variable : values.variables()) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSet.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSet.java index c19e72cd6..d5ce7ecaa 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSet.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSet.java @@ -7,10 +7,11 @@ import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; import fr.inria.corese.core.next.query.impl.result.CoreseBinding; +import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; -import java.util.stream.Collectors; /** * Adapts a KGRAM {@link Mapping} to the {@link BindingSet} API. @@ -20,15 +21,47 @@ public final class CoreseBindingSet implements BindingSet { private final Mapping mapping; private final Set visibleNames; + /** + * Adapts a completed solution without exposing its mutable variable-name index. + * + * @param mapping completed solution, no longer modified by its evaluator + * @throws NullPointerException if the mapping is null + */ public CoreseBindingSet(Mapping mapping) { - this(mapping, mapping.getVariableNames()); + this(mapping, Set.copyOf(mapping.getVariableNames())); } + /** + * Shares a result-owned projection, filtering only unbound variables. + * + * @param mapping completed solution, no longer modified by its evaluator + * @param visibleNames immutable projection shared across the result's rows + * @throws NullPointerException if the mapping or projection is null + */ CoreseBindingSet(Mapping mapping, Set visibleNames) { this.mapping = Objects.requireNonNull(mapping, "mapping"); - this.visibleNames = visibleNames.stream() - .filter(name -> mapping.getValue(name) != null) - .collect(Collectors.toUnmodifiableSet()); + this.visibleNames = filterVisible(mapping, visibleNames); + } + + @SuppressWarnings("java:S9391") // Avoid stream and intermediate allocation per emitted solution row. + private static Set filterVisible(Mapping mapping, Set visibleNames) { + boolean allPresent = true; + for (String name : visibleNames) { + if (mapping.getValue(name) == null) { + allPresent = false; + break; + } + } + if (allPresent) { + return visibleNames; + } + Set present = LinkedHashSet.newLinkedHashSet(visibleNames.size()); + for (String name : visibleNames) { + if (mapping.getValue(name) != null) { + present.add(name); + } + } + return Collections.unmodifiableSet(present); } @Override diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/PreparedQueryPlan.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/PreparedQueryPlan.java new file mode 100644 index 000000000..7daa6fb0c --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/PreparedQueryPlan.java @@ -0,0 +1,116 @@ +package fr.inria.corese.core.next.query.impl.sparql.execution; + +import fr.inria.corese.core.next.data.api.term.IRI; +import fr.inria.corese.core.next.query.api.dataset.Dataset; +import fr.inria.corese.core.next.query.impl.engine.diagnostics.ExecutionDiagnostics; +import fr.inria.corese.core.next.query.impl.engine.model.Edge; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.pattern.CompiledQueryPlan; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; +import fr.inria.corese.core.next.query.impl.engine.storage.StorageManagerProducer; +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; +import fr.inria.corese.core.next.storage.api.StorageManager; + +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Prepared-query-owned compilation cache, confined to its connection's calling + * thread. The AST and cached compilation image carry no execution state. Bindings + * and timeouts are deliberately absent from the cache key: cardinalities describe + * the unbound pattern, and runtime bindings are supplied to a fresh Eval/Memory. + */ +public final class PreparedQueryPlan { + private final QueryAst ast; + private CachedPlan cached; + + PreparedQueryPlan(QueryAst ast) { + this.ast = Objects.requireNonNull(ast, "ast"); + } + + Query resolve(SparqlQueryExecutor executor, StorageManager storage, Dataset dataset, + ExecutionDiagnostics diagnostics) { + long version = storage.queries().getDataVersion(); + CachedPlan previous = cached; + if (version >= 0 && previous != null && previous.version() == version + && sameDataset(previous.dataset(), dataset)) { + return instantiate(previous.plan(), diagnostics); + } + Query compiled = executor.compile(ast); + if (dataset != null) executor.applyDataset(compiled, dataset); + RecordingProducer statistics = new RecordingProducer(storage); + long started = diagnostics == null ? 0 : System.nanoTime(); + compiled.complete(statistics); + long planning = diagnostics == null ? 0 : System.nanoTime() - started; + if (version < 0) { + if (diagnostics != null) { + statistics.counts.forEach(diagnostics::estimated); + diagnostics.planned(compiled, planning); + } + return compiled; + } + started = diagnostics == null ? 0 : System.nanoTime(); + CompiledQueryPlan image = CompiledQueryPlan.capture(compiled, statistics.counts); + if (diagnostics != null) diagnostics.compilationTime(System.nanoTime() - started); + // Do not label a plan with a version published by a racing writer. Estimates + // can be stale during concurrent writes, but are never used to prune results. + if (version >= 0 && storage.queries().getDataVersion() == version) { + cached = new CachedPlan(version, dataset, image); + } + // Capture has already detached the image. The freshly compiled graph can + // serve this first execution directly; no second structural copy is needed. + if (diagnostics != null) { + statistics.counts.forEach(diagnostics::estimated); + diagnostics.planned(compiled, planning); + } + return compiled; + } + + private static Query instantiate(CompiledQueryPlan image, ExecutionDiagnostics diagnostics) { + long started = diagnostics == null ? 0 : System.nanoTime(); + Query query = image.newExecution(diagnostics); + if (diagnostics != null) { + diagnostics.executionSetupTime(System.nanoTime() - started); + diagnostics.reusedPlan(true); + diagnostics.planned(query, 0); + } + return query; + } + + private static boolean sameDataset(Dataset previous, Dataset current) { + return previous == current || previous != null && current != null + && sameOrder(previous.getDefaultGraphs(), current.getDefaultGraphs()) + && sameOrder(previous.getNamedGraphs(), current.getNamedGraphs()); + } + + private static boolean sameOrder(Set previous, Set current) { + if (previous.size() != current.size()) return false; + Iterator left = previous.iterator(); + Iterator right = current.iterator(); + while (left.hasNext()) { + if (!left.next().equals(right.next())) return false; + } + return true; + } + + /** Counts only during a cache miss; execution uses its separate ordinary/diagnostic producer. */ + private static final class RecordingProducer extends StorageManagerProducer { + private final Map counts = new IdentityHashMap<>(); + + private RecordingProducer(StorageManager storage) { + super(storage); + } + + @Override + public long estimateCardinality(Query query, Node graph, Edge edge) { + long count = super.estimateCardinality(query, graph, edge); + counts.put(edge, count); + return count; + } + } + + private record CachedPlan(long version, Dataset dataset, CompiledQueryPlan plan) { } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/SparqlQueryExecutor.java similarity index 65% rename from src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java rename to src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/SparqlQueryExecutor.java index 9c8c5b303..d3cf94096 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/SparqlQueryExecutor.java @@ -1,6 +1,5 @@ package fr.inria.corese.core.next.query.impl.sparql.execution; - import fr.inria.corese.core.next.data.Values; import fr.inria.corese.core.next.data.api.term.IRI; import fr.inria.corese.core.next.data.api.term.Resource; @@ -8,6 +7,9 @@ import fr.inria.corese.core.next.data.api.term.Value; import fr.inria.corese.core.next.data.api.factory.ValueFactory; import fr.inria.corese.core.next.query.api.dataset.Dataset; +import fr.inria.corese.core.next.query.impl.engine.diagnostics.DiagnosticProducer; +import fr.inria.corese.core.next.query.impl.engine.diagnostics.ExecutionDiagnostics; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.FallbackReason; import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; import fr.inria.corese.core.next.query.api.exception.QueryTimeoutException; import fr.inria.corese.core.next.query.api.result.Binding; @@ -26,16 +28,17 @@ import fr.inria.corese.core.next.query.impl.engine.model.Edge; import fr.inria.corese.core.next.query.impl.engine.model.Node; import fr.inria.corese.core.next.query.impl.engine.eval.Eval; +import fr.inria.corese.core.next.query.impl.engine.eval.SelectCursor; import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; import fr.inria.corese.core.next.query.impl.engine.pattern.Query; -import fr.inria.corese.core.next.query.impl.engine.eval.SparqlException; 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.model.NodeImpl; import fr.inria.corese.core.next.query.impl.engine.storage.StorageManagerProducer; import fr.inria.corese.core.next.storage.api.StorageManager; +import fr.inria.corese.core.next.storage.api.exception.StorageException; import java.util.ArrayList; import java.util.HashMap; @@ -56,11 +59,11 @@ * SPARQL string -> next parser -> next AST -> next KGRAM -> StorageManagerProducer -> StorageManager * * - *

    The executor handles SELECT, ASK, CONSTRUCT, and DESCRIBE through the next - * storage path. Individual SPARQL 1.1 algebra features are enabled explicitly - * as their runtime implementations become available.

    + *

    The executor handles SELECT, ASK, CONSTRUCT, and DESCRIBE through the native + * storage path. Each evaluation owns its producer, runtime bindings, and timeout + * task; closing the producer releases any statement cursors left by early termination.

    */ -public final class NextSparqlPipelineExecutor { +public final class SparqlQueryExecutor { private static final int QUERY_RUNNING = 0; private static final int QUERY_COMPLETED = 1; @@ -69,6 +72,7 @@ public final class NextSparqlPipelineExecutor { private final StorageManager storage; private final SparqlParser parser; private final CoreseAstQueryBuilder queryBuilder; + private final ExecutionDiagnostics diagnostics; /** * Creates an executor backed by the given next storage manager. @@ -76,12 +80,12 @@ public final class NextSparqlPipelineExecutor { * @param storage storage manager used by {@link StorageManagerProducer} to * read RDF statements during KGRAM evaluation */ - public NextSparqlPipelineExecutor(StorageManager storage) { + public SparqlQueryExecutor(StorageManager storage) { this(storage, new SparqlParser(), new CoreseAstQueryBuilder()); } /** Creates an executor with a query-scoped source base IRI. */ - public NextSparqlPipelineExecutor(StorageManager storage, String baseIRI) { + public SparqlQueryExecutor(StorageManager storage, String baseIRI) { this(storage, new SparqlParser(new SparqlParserOptions.Builder().baseIRI(baseIRI).build()), new CoreseAstQueryBuilder()); } @@ -92,19 +96,69 @@ public NextSparqlPipelineExecutor(StorageManager storage, String baseIRI) { *

    This constructor is package-private so tests can inject parser or bridge * variants without exposing wiring details as public API.

    */ - NextSparqlPipelineExecutor( + SparqlQueryExecutor( StorageManager storage, SparqlParser parser, CoreseAstQueryBuilder queryBuilder) { + this(storage, parser, queryBuilder, null); + } + + private SparqlQueryExecutor(StorageManager storage, SparqlParser parser, + CoreseAstQueryBuilder queryBuilder, ExecutionDiagnostics diagnostics) { this.storage = Objects.requireNonNull(storage, "storage"); this.parser = Objects.requireNonNull(parser, "parser"); this.queryBuilder = Objects.requireNonNull(queryBuilder, "queryBuilder"); + this.diagnostics = diagnostics; } // ------------------------------------------------------------------------- // Public API — called by CoreseTupleQuery / CoreseBooleanQuery // ------------------------------------------------------------------------- + /** + * Creates a query-owned cache around an already prepared AST, parsing only when absent. + * + * @param sparql original query text + * @param parsed already parsed query or null + * @return prepared compilation cache + */ + public PreparedQueryPlan prepare(String sparql, QueryAst parsed) { + return new PreparedQueryPlan(parsed == null ? parser.parse(sparql) : parsed); + } + + /** Evaluates a prepared SELECT using a version-validated compilation image. */ + public TupleQueryResult evaluateTuple(PreparedQueryPlan prepared, BindingSet bindings, + Dataset dataset, long timeoutMillis) { + return evaluateTuple(new ExecutionRequest(prepared, dataset), bindings, timeoutMillis); + } + + /** Evaluates a prepared ASK using a version-validated compilation image. */ + public boolean evaluateBoolean(PreparedQueryPlan prepared, BindingSet bindings, + Dataset dataset, long timeoutMillis) { + return evaluate(new ExecutionRequest(prepared, dataset), bindings, timeoutMillis).size() > 0; + } + + /** Evaluates a prepared CONSTRUCT/DESCRIBE with fresh execution and blank-node state. */ + public GraphQueryResult evaluateGraph(PreparedQueryPlan prepared, BindingSet bindings, + Dataset dataset, long timeoutMillis) { + ExecutionRequest request = new ExecutionRequest(prepared, dataset); + Mappings mappings = evaluate(request, bindings, timeoutMillis); + return new CoreseGraphQueryResult(buildConstructStatements(request.query, mappings).iterator()); + } + + /** + * Creates an executor sharing compilation collaborators for one profiled call. + * The original executor remains uninstrumented. Neither executor is thread-safe. + * + * @param collector request-scoped measurements + * @return executor instrumented with the supplied collector + * @throws NullPointerException if the collector is null + */ + public SparqlQueryExecutor withDiagnostics(ExecutionDiagnostics collector) { + return new SparqlQueryExecutor(storage, parser, queryBuilder, + Objects.requireNonNull(collector, "collector")); + } + /** * Evaluates a SELECT query through the next pipeline with no initial bindings, * no dataset override, and no timeout. @@ -146,7 +200,93 @@ public TupleQueryResult evaluateTuple(SelectQueryAst query) { /** Evaluates a parsed SELECT with current options and a fresh runtime query. */ public TupleQueryResult evaluateTuple(SelectQueryAst query, BindingSet bindings, Dataset dataset, long timeoutMillis) { - return new CoreseTupleQueryResult(evaluate(queryBuilder.toNextQuery(query), bindings, dataset, timeoutMillis)); + return evaluateTuple(new ExecutionRequest(compile(query), dataset), bindings, timeoutMillis); + } + + private TupleQueryResult evaluateTuple(ExecutionRequest request, BindingSet bindings, long timeoutMillis) { + if (diagnostics != null) diagnostics.beginWork(); + try { + return openTuple(request, bindings, timeoutMillis); + } catch (StorageException failure) { + throw new QueryEvaluationException("Storage access failed during query evaluation", failure); + } finally { + if (diagnostics != null) diagnostics.endWork(); + } + } + + private TupleQueryResult openTuple(ExecutionRequest request, BindingSet bindings, long timeoutMillis) { + long started = System.nanoTime(); + Query query = resolveExecutionQuery(request); + FallbackReason reason = SelectCursor.fallbackReason(query); + boolean ordered = reason == FallbackReason.ORDER_BY && SelectCursor.supportsOrderedInput(query); + if (reason == FallbackReason.NONE || ordered) { + return snapshotTuple(request, bindings, timeoutMillis, started, ordered); + } + if (diagnostics != null) diagnostics.selectDelivery(reason); + return materializedTuple(request, bindings, timeoutMillis, started); + } + + private Query resolveExecutionQuery(ExecutionRequest request) { + if (request.prepared != null) { + return resolve(request, null); + } + StorageManagerProducer planning = diagnostics == null + ? new StorageManagerProducer(storage) : new DiagnosticProducer(storage, diagnostics); + Query query = resolve(request, planning); + query.complete(planning); + return query; + } + + private TupleQueryResult snapshotTuple(ExecutionRequest request, BindingSet bindings, + long timeoutMillis, long started, boolean ordered) { + StorageManager snapshot; + try { + snapshot = storage.openReadSnapshot(); + } catch (UnsupportedOperationException unavailable) { + if (diagnostics != null) diagnostics.selectDelivery(FallbackReason.SNAPSHOT_UNAVAILABLE); + return materializedTuple(request, bindings, timeoutMillis, started); + } + if (diagnostics != null) diagnostics.selectDelivery(ordered ? FallbackReason.ORDER_BY : FallbackReason.NONE); + return evaluateSnapshotTuple(request.query, bindings, timeoutMillis, started, snapshot, ordered); + } + + @SuppressWarnings("java:S1181") // Snapshot ownership is released on fatal setup errors before rethrowing unchanged. + private TupleQueryResult evaluateSnapshotTuple(Query query, BindingSet bindings, + long timeoutMillis, long started, StorageManager snapshot, boolean ordered) { + StorageManagerProducer producer = null; + Eval eval; + StreamingTupleQueryResult result; + try { + producer = diagnostics == null + ? new StorageManagerProducer(snapshot) : new DiagnosticProducer(snapshot, diagnostics); + eval = Eval.create(producer, new SparqlKgramEvaluator(), new RdfTermMatcher()); + long initialization = diagnostics == null ? 0 : System.nanoTime(); + Mapping initial = buildInitialMapping(bindings); + SelectCursor cursor = ordered ? SelectCursor.orderedInput(eval, query, initial) + : new SelectCursor(eval, query, initial); + if (diagnostics != null) diagnostics.evaluationTime(System.nanoTime() - initialization); + result = new StreamingTupleQueryResult(cursor, query, producer, snapshot, + diagnostics, timeoutMillis, started); + } catch (RuntimeException | Error failure) { + try { + if (producer != null) producer.close(); + } finally { + snapshot.close(); + } + throw failure; + } + if (!ordered) return result; + try (result) { + return new CoreseTupleQueryResult(result.collectOrdered(new TopKSelector(query, eval))); + } + } + + private TupleQueryResult materializedTuple(ExecutionRequest request, BindingSet bindings, + long timeoutMillis, long started) { + Mappings mappings = evaluate(request, bindings, timeoutMillis, started); + TupleQueryResult result = new CoreseTupleQueryResult(mappings); + if (diagnostics != null) diagnostics.firstRow(!mappings.isEmpty()); + return result; } /** @@ -185,7 +325,7 @@ public boolean evaluateBoolean(String sparql, BindingSet bindings, Dataset datas /** Evaluates a parsed ASK with current options and a fresh runtime query. */ public boolean evaluateBoolean(AskQueryAst query, BindingSet bindings, Dataset dataset, long timeoutMillis) { - return evaluate(queryBuilder.toNextQuery(query), bindings, dataset, timeoutMillis).size() > 0; + return evaluate(compile(query), bindings, dataset, timeoutMillis).size() > 0; } /** @@ -225,13 +365,11 @@ public GraphQueryResult evaluateGraph(String sparql, BindingSet bindings, Datase /** Evaluates a parsed CONSTRUCT or DESCRIBE with a fresh runtime query and template. */ public GraphQueryResult evaluateGraph(QueryAst ast, BindingSet bindings, Dataset dataset, long timeoutMillis) { Objects.requireNonNull(ast, "ast"); - Query kgramQuery = switch (ast) { - case ConstructQueryAst construct -> queryBuilder.toNextQuery(construct); - case DescribeQueryAst describe -> queryBuilder.toNextQuery(describe); - default -> throw new IllegalArgumentException( - "Graph evaluation requires a CONSTRUCT or DESCRIBE query, got: " - + ast.getClass().getSimpleName()); - }; + if (!(ast instanceof ConstructQueryAst || ast instanceof DescribeQueryAst)) { + throw new IllegalArgumentException("Graph evaluation requires a CONSTRUCT or DESCRIBE query, got: " + + ast.getClass().getSimpleName()); + } + Query kgramQuery = compile(ast); Mappings mappings = evaluate(kgramQuery, bindings, dataset, timeoutMillis); List statements = buildConstructStatements(kgramQuery, mappings); return new CoreseGraphQueryResult(statements.iterator()); @@ -241,31 +379,94 @@ public GraphQueryResult evaluateGraph(QueryAst ast, BindingSet bindings, Dataset // Internal evaluation pipeline // ------------------------------------------------------------------------- - private Mappings evaluate(Query kgramQuery, BindingSet bindings, Dataset dataset, long timeoutMillis) { - StorageManagerProducer producer = new StorageManagerProducer(storage); - Eval eval = Eval.create( - producer, - new SparqlKgramEvaluator(), - new RdfTermMatcher()); - - if (dataset != null) { - applyDataset(kgramQuery, dataset); + Query compile(QueryAst ast) { + if (diagnostics == null) return compileAst(ast); + diagnostics.beginWork(); + long started = System.nanoTime(); + try { + return compileAst(ast); + } finally { + diagnostics.compilationTime(System.nanoTime() - started); + diagnostics.endWork(); } + } - Mapping initialMapping = buildInitialMapping(bindings); + private Query compileAst(QueryAst ast) { + return switch (ast) { + case SelectQueryAst select -> queryBuilder.toNextQuery(select); + case AskQueryAst ask -> queryBuilder.toNextQuery(ask); + case ConstructQueryAst construct -> queryBuilder.toNextQuery(construct); + case DescribeQueryAst describe -> queryBuilder.toNextQuery(describe); + default -> throw new IllegalArgumentException("Expected a query, got " + ast.getClass().getSimpleName()); + }; + } + + private Mappings evaluate(Query query, BindingSet bindings, Dataset dataset, long timeoutMillis) { + return evaluate(new ExecutionRequest(query, dataset), bindings, timeoutMillis); + } + + private Mappings evaluate(ExecutionRequest request, BindingSet bindings, long timeoutMillis) { + return evaluate(request, bindings, timeoutMillis, System.nanoTime()); + } + private Mappings evaluate(ExecutionRequest request, BindingSet bindings, long timeoutMillis, long started) { + StorageManagerProducer producer = diagnostics == null + ? new StorageManagerProducer(storage) : new DiagnosticProducer(storage, diagnostics); + Eval eval = Eval.create(producer, new SparqlKgramEvaluator(), new RdfTermMatcher()); + Mapping initialMapping = buildInitialMapping(bindings); if (timeoutMillis > 0) { - return evaluateWithTimeout(eval, producer, kgramQuery, initialMapping, timeoutMillis); + return evaluateWithTimeout(eval, producer, request, initialMapping, timeoutMillis, started); } - return evaluateCore(eval, kgramQuery, initialMapping); + return evaluateCore(eval, request, initialMapping); } - private Mappings evaluateCore(Eval eval, Query kgramQuery, Mapping initialMapping) { + private Mappings evaluateCore(Eval eval, ExecutionRequest request, Mapping initialMapping) { try { - return eval.query(null, kgramQuery, initialMapping); - } catch (SparqlException e) { - throw new QueryEvaluationException( - "Failed to evaluate query with the next pipeline: " + e.getMessage(), e); + Query query = resolve(request, eval.getProducer()); + if (diagnostics == null) { + return eval.query(null, query, initialMapping); + } + long started = System.nanoTime(); + Mappings mappings = eval.query(null, query, initialMapping); + diagnostics.evaluationTime(System.nanoTime() - started); + return mappings; + } catch (StorageException failure) { + throw new QueryEvaluationException("Storage access failed during query evaluation", failure); + } + } + + private Query resolve(ExecutionRequest request, fr.inria.corese.core.next.query.impl.engine.spi.Producer producer) { + if (request.resolved) return request.query; + if (request.prepared != null) { + request.query = request.prepared.resolve(this, storage, request.dataset, diagnostics); + } else { + if (request.dataset != null) applyDataset(request.query, request.dataset); + if (diagnostics != null) { + long started = System.nanoTime(); + request.query.complete(producer); + diagnostics.planned(request.query, System.nanoTime() - started); + } + } + request.resolved = true; + return request.query; + } + + /** Mutable request state; never retained in the prepared compilation cache. */ + private static final class ExecutionRequest { + private final PreparedQueryPlan prepared; + private final Dataset dataset; + private Query query; + private boolean resolved; + + private ExecutionRequest(PreparedQueryPlan prepared, Dataset dataset) { + this.prepared = Objects.requireNonNull(prepared, "prepared"); + this.dataset = dataset; + } + + private ExecutionRequest(Query query, Dataset dataset) { + this.prepared = null; + this.query = query; + this.dataset = dataset; } } @@ -279,10 +480,9 @@ private Mappings evaluateCore(Eval eval, Query kgramQuery, Mapping initialMappin * an expiration separately from claiming it. Elapsed time is measured from entry, * including watchdog scheduling delay.

    */ - private Mappings evaluateWithTimeout(Eval eval, StorageManagerProducer producer, Query kgramQuery, Mapping initialMapping, long timeoutMillis) { + private Mappings evaluateWithTimeout(Eval eval, StorageManagerProducer producer, ExecutionRequest request, Mapping initialMapping, long timeoutMillis, long started) { Thread caller = Thread.currentThread(); boolean interruptedOnEntry = caller.isInterrupted(); - long started = System.nanoTime(); long budget = TimeUnit.MILLISECONDS.toNanos(timeoutMillis); AtomicInteger outcome = new AtomicInteger(QUERY_RUNNING); Thread watchdog = Thread.ofVirtual().name("sparql-timeout-watchdog").start(() -> { @@ -302,7 +502,7 @@ private Mappings evaluateWithTimeout(Eval eval, StorageManagerProducer producer, }); try { - Mappings mappings = evaluateCore(eval, kgramQuery, initialMapping); + Mappings mappings = evaluateCore(eval, request, initialMapping); if (!outcome.compareAndSet(QUERY_RUNNING, QUERY_COMPLETED) || System.nanoTime() - started >= budget) { throw new QueryTimeoutException(timeoutMillis); @@ -478,7 +678,7 @@ private Value kgramNodeToApiValue(Node node) { * sets remain significant: for example, a named-only dataset has an empty * default graph.

    */ - private void applyDataset(Query kgramQuery, Dataset dataset) { + void applyDataset(Query kgramQuery, Dataset dataset) { kgramQuery.setFrom(irisToKgramNodes(dataset.getDefaultGraphs())); kgramQuery.setNamed(irisToKgramNodes(dataset.getNamedGraphs())); kgramQuery.setDatasetSpecified(true); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/StreamingTupleQueryResult.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/StreamingTupleQueryResult.java new file mode 100644 index 000000000..05469415c --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/StreamingTupleQueryResult.java @@ -0,0 +1,256 @@ +package fr.inria.corese.core.next.query.impl.sparql.execution; + +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.exception.QueryTimeoutException; +import fr.inria.corese.core.next.query.api.result.BindingSet; +import fr.inria.corese.core.next.query.api.result.TupleQueryResult; +import fr.inria.corese.core.next.query.impl.engine.diagnostics.ExecutionDiagnostics; +import fr.inria.corese.core.next.query.impl.engine.eval.SelectCursor; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; +import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; +import fr.inria.corese.core.next.query.impl.engine.storage.StorageManagerProducer; +import fr.inria.corese.core.next.storage.api.StorageManager; +import fr.inria.corese.core.next.storage.api.exception.StorageException; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +/** + * Caller-thread SELECT result owning its complete read view. Advancing is confined + * to the creating thread; close may cancel it from another thread. A deadline spans + * preparation, traversal and client idle time, but interrupts the owner only while + * it is advancing this cursor. Expiration releases snapshots even for an idle client. + */ +final class StreamingTupleQueryResult implements TupleQueryResult { + /** Shared daemon timers; cancelled tasks must not retain closed query snapshots. */ + private static final ScheduledThreadPoolExecutor DEADLINES = deadlines(); + + private static ScheduledThreadPoolExecutor deadlines() { + var timers = new ScheduledThreadPoolExecutor(2, + Thread.ofPlatform().daemon(true).name("sparql-cursor-timeout-", 0).factory()); + timers.setRemoveOnCancelPolicy(true); + return timers; + } + + private final Object cancellation = new Object(); + private final Thread owner = Thread.currentThread(); + private final SelectCursor cursor; + private final StorageManagerProducer producer; + private final StorageManager snapshot; + private final ExecutionDiagnostics diagnostics; + private final List names; + private final Set projected; + private final long timeoutMillis; + private final long started; + private final ScheduledFuture deadline; + private boolean advancing; + private boolean viewReleased; + private volatile boolean timedOut; + private volatile boolean closed; + private boolean exhausted; + private BindingSet delivered; + private boolean unwound; + + StreamingTupleQueryResult(SelectCursor cursor, Query query, + StorageManagerProducer producer, StorageManager snapshot, ExecutionDiagnostics diagnostics, + long timeoutMillis, long started) { + this.cursor = cursor; + this.producer = producer; + this.snapshot = snapshot; + this.diagnostics = diagnostics; + this.timeoutMillis = timeoutMillis; + this.started = started; + names = query.getSelect().stream().map(Node::getLabel).toList(); + projected = Set.copyOf(names); + long remaining = TimeUnit.MILLISECONDS.toNanos(timeoutMillis) - (System.nanoTime() - started); + deadline = timeoutMillis <= 0 ? null + : DEADLINES.schedule(this::expire, Math.max(0, remaining), TimeUnit.NANOSECONDS); + } + + private void expire() { + synchronized (cancellation) { + if (closed || exhausted || timedOut) return; + timedOut = true; + if (advancing) owner.interrupt(); + cursor.cancel(); + releaseView(); + } + } + + @Override + public List getBindingNames() { + return names; + } + + @Override + public boolean hasNext() { + return advance(false); + } + + @SuppressWarnings("java:S1181") // Release owned snapshots even on fatal traversal errors, then rethrow unchanged. + private boolean advance(boolean consume) { + checkOwner(); + if (diagnostics != null) diagnostics.beginWork(); + try { + if (!beginAdvance()) return false; + boolean more = cursor.hasNext(); + if (more && consume) delivered = new CoreseBindingSet(cursor.next(), projected); + endAdvance(); + if (!more || cursor.isFinished()) finish(); + if (diagnostics != null) diagnostics.firstRow(more); + return more; + } catch (RuntimeException | Error failure) { + traversalFailed(failure); + throw failure; + } finally { + if (diagnostics != null) diagnostics.evaluationTime(diagnostics.endWork()); + } + } + + /** + * Consumes unsliced ordered input under the same snapshot and deadline as a + * progressive read. Only the bounded selection is materialized, not all rows. + * + * @param selector evaluation-owned collector using Corese's ordering comparator + * @return ordered final mappings after LIMIT and OFFSET + */ + @SuppressWarnings("java:S1181") // Fatal traversal failures must release the same owned resources as ordinary reads. + Mappings collectOrdered(TopKSelector selector) { + checkOwner(); + long startedWork = diagnostics == null ? 0 : System.nanoTime(); + if (diagnostics != null) diagnostics.beginWork(); + try { + beginAdvance(); + if (selector.needsInput()) { + while (cursor.hasNext()) selector.offer(cursor.next()); + } + Mappings selected = selector.finish(); + endAdvance(); + finish(); + if (diagnostics != null) diagnostics.firstRow(!selected.isEmpty()); + return selected; + } catch (RuntimeException | Error failure) { + traversalFailed(failure); + throw failure; + } finally { + if (diagnostics != null) { + diagnostics.endWork(); + diagnostics.evaluationTime(System.nanoTime() - startedWork); + } + } + } + + private void traversalFailed(Throwable failure) { + failedAdvance(failure); + if (timedOut && !(failure instanceof Error)) throw new QueryTimeoutException(timeoutMillis); + if (failure instanceof StorageException) { + throw new QueryEvaluationException("Storage access failed during query evaluation", failure); + } + } + + private boolean beginAdvance() { + synchronized (cancellation) { + if (!closed && !exhausted && timeoutMillis > 0 + && System.nanoTime() - started >= TimeUnit.MILLISECONDS.toNanos(timeoutMillis)) expire(); + if (timedOut) throw new QueryTimeoutException(timeoutMillis); + if (closed) throw new IllegalStateException("This tuple query result is closed."); + if (exhausted) return false; + if (owner.isInterrupted()) throw new QueryEvaluationException("Query interrupted"); + advancing = true; + return true; + } + } + + private void endAdvance() { + synchronized (cancellation) { + advancing = false; + if (timedOut) { + Thread.interrupted(); + throw new QueryTimeoutException(timeoutMillis); + } + if (closed) throw new QueryEvaluationException("Query cursor was cancelled"); + } + } + + @SuppressWarnings("java:S1181") // Preserve the traversal failure if releasing resources also fails. + private void failedAdvance(Throwable failure) { + synchronized (cancellation) { + if (timedOut && advancing) Thread.interrupted(); + advancing = false; + } + try { + close(false); + } catch (RuntimeException | Error cleanup) { + if (cleanup != failure) failure.addSuppressed(cleanup); + } + } + + @Override + public BindingSet next() { + if (!advance(true)) throw new NoSuchElementException(); + BindingSet row = delivered; + delivered = null; + return row; + } + + private void checkOwner() { + if (Thread.currentThread() != owner) { + throw new IllegalStateException("Query results must be consumed on their creating thread"); + } + } + + private void finish() { + synchronized (cancellation) { + exhausted = true; + if (deadline != null) deadline.cancel(false); + releaseView(); + } + } + + /** Called with the cancellation monitor; read-snapshot close is cross-thread safe. */ + private void releaseView() { + if (viewReleased) return; + viewReleased = true; + try { + producer.close(); + } finally { + snapshot.close(); + } + } + + @Override + public void close() { + close(true); + } + + private void close(boolean measureClose) { + boolean onOwner = Thread.currentThread() == owner; + boolean measure = measureClose && onOwner && !unwound && diagnostics != null; + if (measure) diagnostics.beginWork(); + try { + synchronized (cancellation) { + closed = true; + if (advancing && Thread.currentThread() != owner) owner.interrupt(); + if (deadline != null) deadline.cancel(false); + cursor.cancel(); + releaseView(); + } + } finally { + // Only the owner unwinds bindings. Cancellation already releases storage. + if (onOwner) { + delivered = null; + unwound = true; + try { + cursor.close(); + } finally { + if (measure) diagnostics.evaluationTime(diagnostics.endWork()); + } + } + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/TopKSelector.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/TopKSelector.java new file mode 100644 index 000000000..33bdaea75 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/TopKSelector.java @@ -0,0 +1,104 @@ +package fr.inria.corese.core.next.query.impl.sparql.execution; + +import fr.inria.corese.core.next.query.impl.engine.eval.Eval; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; +import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; +import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; + +import java.util.ArrayList; +import java.util.PriorityQueue; + +/** + * Evaluation-thread bounded ORDER BY collector. The worst retained solution is + * at the heap root. Arrival ordinals preserve the stable full sort's tie order, + * including ties crossing OFFSET or the selection boundary. Projection and + * DISTINCT must already have been applied by the input cursor. + * + * Only retained slots allocate ranking wrappers: replacement reuses the evicted + * slot. Candidate Mapping objects are still produced by the ordinary evaluator. + */ +final class TopKSelector { + private final Mappings ordering; + private final PriorityQueue heap; + private final int capacity; + private final int offset; + private final boolean needsInput; + private long arrival; + + /** + * Creates a selector without eagerly reserving a potentially large LIMIT. + * + * @param query completed eligible ordered SELECT with finite LIMIT plus OFFSET + * @param eval evaluator supplying the official Corese ordering comparator + */ + TopKSelector(Query query, Eval eval) { + capacity = Math.addExact(query.getLimit(), query.getOffset()); + offset = query.getOffset(); + needsInput = query.getLimit() != 0; + ordering = Mappings.create(query); + ordering.setEval(eval); + heap = new PriorityQueue<>(Math.clamp(capacity, 1, 16), this::worstFirst); + } + + /** @return whether a nonzero LIMIT requires evaluating the input */ + boolean needsInput() { + return needsInput; + } + + /** + * Retains a candidate only if it improves the bounded selection. Equal later + * candidates cannot displace an earlier one at the selection boundary. + * + * @param row projected and deduplicated solution + */ + void offer(Mapping row) { + long ordinal = arrival++; + if (heap.size() < capacity) { + heap.add(new RankedMapping(row, ordinal)); + } else if (capacity > 0 && ordering.compare(row, heap.element().row) < 0) { + RankedMapping slot = heap.remove(); + slot.row = row; + slot.arrival = ordinal; + heap.add(slot); + } + } + + private int bestFirst(RankedMapping left, RankedMapping right) { + int comparison = ordering.compare(left.row, right.row); + return comparison == 0 ? Long.compare(left.arrival, right.arrival) : comparison; + } + + @SuppressWarnings("java:S2234") // Reverse the stable best-first comparison so the worst retained row heads the heap. + private int worstFirst(RankedMapping left, RankedMapping right) { + return bestFirst(right, left); + } + + /** + * Sorts at most K winners and applies OFFSET once. No evaluator, heap slots + * or discarded candidates escape with the returned final mappings. + * + * @return selected mappings in stable Corese order + */ + Mappings finish() { + try { + var winners = new ArrayList<>(heap); + winners.sort(this::bestFirst); + for (int i = offset; i < winners.size(); i++) ordering.add(winners.get(i).row); + return ordering; + } finally { + heap.clear(); + ordering.setEval(null); + } + } + + /** Mutable heap slot, reused after eviction rather than allocated for every candidate. */ + private static final class RankedMapping { + private Mapping row; + private long arrival; + + private RankedMapping(Mapping row, long arrival) { + this.row = row; + this.arrival = arrival; + } + } +} 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 afee85e9c..4fb05e51f 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 @@ -185,10 +185,11 @@ public void enterGraph(TermAst name) { /** Expression and constraint building helpers. */ protected final SparqlExpressionBuilder expressions; + @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(options, this::termFromBlankNode); + this.terms = new SparqlTermBuilder(this::termFromBlankNode); this.paths = new SparqlPathBuilder(this.terms); this.expressions = new SparqlExpressionBuilder(this.terms, this::popCapturedExistsPattern); } @@ -843,6 +844,7 @@ private TermAst rdfTerm(RDF term) { /** * 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()) { 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 e28255438..53269c457 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 @@ -32,6 +32,7 @@ final class SparqlQuadTemplateBuilder { * @param context the ANTLR quads context * @return the lowered {@link QuadsAst} */ + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. QuadsAst quads(SparqlParser.QuadsContext context) { List defaults = new ArrayList<>(); List named = new ArrayList<>(); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlTermBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlTermBuilder.java index 7cc77d6ed..238107d3c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlTermBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlTermBuilder.java @@ -34,16 +34,12 @@ public final class SparqlTermBuilder { /** * Constructs a {@code SparqlTermBuilder} with a blank-node resolver callback. * - * @param options parser options validated for non-null configuration - * (reserved for future term resolution extensions) * @param blankNodeResolver callback that handles {@code BlankNodeContext} and * updates the mutable blank-node label set held by * {@link SparqlAstBuilder} */ - public SparqlTermBuilder(SparqlParserOptions options, - Function blankNodeResolver) { - Objects.requireNonNull(options, "options"); - this.blankNodeResolver = blankNodeResolver; + public SparqlTermBuilder(Function blankNodeResolver) { + this.blankNodeResolver = Objects.requireNonNull(blankNodeResolver, "blankNodeResolver"); } /** @@ -51,10 +47,10 @@ public SparqlTermBuilder(SparqlParserOptions options, * Use this only when {@link #termFromGraphTerm} will never encounter a * {@code blankNode()} alternative. */ - public SparqlTermBuilder(SparqlParserOptions options) { - this(options, ctx -> { + public SparqlTermBuilder() { + this(ctx -> { throw new UnsupportedOperationException( - "Blank node resolution requires a blankNodeResolver — use the two-argument constructor"); + "Blank node resolution requires a blankNodeResolver — supply a blank-node resolver"); }); } @@ -299,6 +295,7 @@ public TermAst termFromGraphTerm(SparqlParser.GraphTermContext ctx) { * @param ctx the objectList parse context * @param objectConverter function converting a single {@code object_} to a term */ + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. public List termListFromObjectList(SparqlParser.ObjectListContext ctx, Function objectConverter) { List out = new ArrayList<>(); 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 70815e1ad..afac5d47b 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 @@ -40,6 +40,7 @@ private ValuesAst processDataBlock(SparqlParser.DataBlockContext ctx) { throw new QuerySyntaxException("Missing data block in VALUES clause"); } + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. private ValuesAst processInlineDataOneVar(SparqlParser.InlineDataOneVarContext ctx) { if (ctx.var_() == null) { throw new QuerySyntaxException("Missing variable for solution mapping in VALUES clause"); @@ -80,6 +81,7 @@ private Map termAstFromDataBlockValues(List variables, return valuesList; } + @SuppressWarnings("java:S9391") // Preserve ordered accumulation without allocating a stream pipeline. private ValuesAst processInlineDataFull(SparqlParser.InlineDataFullContext ctx) { List varList = new ArrayList<>(); for (var variable : ctx.var_()) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/FilterArgumentsValidationRule.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/FilterArgumentsValidationRule.java index 9f8865135..3c0133dae 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/FilterArgumentsValidationRule.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/FilterArgumentsValidationRule.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.parser.semantic.rule; import fr.inria.corese.core.next.query.api.validation.QueryDiagnostic; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AbstractAstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AbstractAstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.*; import java.util.ArrayList; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/OperandArgumentBooleanTypeValidationRule.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/OperandArgumentBooleanTypeValidationRule.java index 8f93ea591..17c884955 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/OperandArgumentBooleanTypeValidationRule.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/OperandArgumentBooleanTypeValidationRule.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.parser.semantic.rule; import fr.inria.corese.core.next.query.api.validation.QueryDiagnostic; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AbstractAstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AbstractAstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.AndAst; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/OperandArgumentIRITypeValidationRule.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/OperandArgumentIRITypeValidationRule.java index 8c9418e55..f66b43afd 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/OperandArgumentIRITypeValidationRule.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/OperandArgumentIRITypeValidationRule.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.parser.semantic.rule; import fr.inria.corese.core.next.query.api.validation.QueryDiagnostic; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AbstractAstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AbstractAstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/OperandArgumentNumericTypeValidationRule.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/OperandArgumentNumericTypeValidationRule.java index ef344e9d6..2955b82e2 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/OperandArgumentNumericTypeValidationRule.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/rule/OperandArgumentNumericTypeValidationRule.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.parser.semantic.rule; import fr.inria.corese.core.next.query.api.validation.QueryDiagnostic; -import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.AbstractAstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.visitor.AbstractAstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/support/VisitableAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/support/VisitableAst.java deleted file mode 100644 index 8a5ea09a9..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/support/VisitableAst.java +++ /dev/null @@ -1,6 +0,0 @@ -package fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support; - -public interface VisitableAst { - - void accept(AstVisitor visitor); -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/support/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/support/package-info.java index 864fc904c..cb13b9b6d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/support/package-info.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/parser/semantic/support/package-info.java @@ -1,2 +1,5 @@ -/** Internal visitors and helpers shared by SPARQL semantic validation rules. */ +/** + * Variable-scope analysis shared by SPARQL semantic validation rules and compilation. + * Syntax-tree traversal contracts belong to the AST visitor package. + */ package fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/update/NextSparqlUpdateExecutor.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/update/SparqlUpdateExecutor.java similarity index 94% rename from src/main/java/fr/inria/corese/core/next/query/impl/sparql/update/NextSparqlUpdateExecutor.java rename to src/main/java/fr/inria/corese/core/next/query/impl/sparql/update/SparqlUpdateExecutor.java index d0cef4368..2d1a06b65 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/update/NextSparqlUpdateExecutor.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/update/SparqlUpdateExecutor.java @@ -27,7 +27,7 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.UpdateRequestAst; import fr.inria.corese.core.next.query.impl.sparql.ast.UpdateRequestUnitAst; import fr.inria.corese.core.next.query.impl.sparql.bridge.SparqlTermResolver; -import fr.inria.corese.core.next.query.impl.sparql.execution.NextSparqlPipelineExecutor; +import fr.inria.corese.core.next.query.impl.sparql.execution.SparqlQueryExecutor; import fr.inria.corese.core.next.storage.api.StorageManager; import java.util.ArrayList; @@ -36,7 +36,7 @@ /** * Executes SPARQL 1.1 update requests through the native query and storage pipelines. */ -public final class NextSparqlUpdateExecutor { +public final class SparqlUpdateExecutor { private final StorageManager storage; private final QueryPrologueAst prologue; private final SparqlTermResolver resolver; @@ -48,7 +48,7 @@ public final class NextSparqlUpdateExecutor { * @param storage the storage manager to execute mutations and queries against * @param prologue the query prologue containing namespace prefixes and base IRI */ - public NextSparqlUpdateExecutor(StorageManager storage, QueryPrologueAst prologue) { + public SparqlUpdateExecutor(StorageManager storage, QueryPrologueAst prologue) { this.storage = storage; this.prologue = prologue; this.resolver = new SparqlTermResolver(prologue); @@ -63,7 +63,7 @@ public NextSparqlUpdateExecutor(StorageManager storage, QueryPrologueAst prologu */ public void execute(UpdateRequestAst request) { for (int index = 0; index < request.operations().size(); index++) { - new NextSparqlUpdateExecutor(storage, request.operationPrologues().get(index)) + new SparqlUpdateExecutor(storage, request.operationPrologues().get(index)) .execute(request.operations().get(index)); } } @@ -136,7 +136,7 @@ private void modify(ModifyRequestAst operation) { private void apply(SelectQueryAst query, QuadsAst delete, QuadsAst insert, Resource target) { List deletions = new ArrayList<>(); List insertions = new ArrayList<>(); - NextSparqlPipelineExecutor executor = new NextSparqlPipelineExecutor(new UpdateDatasetView(storage, target)); + SparqlQueryExecutor executor = new SparqlQueryExecutor(new UpdateDatasetView(storage, target)); try (TupleQueryResult solutions = executor.evaluateTuple(query)) { while (solutions.hasNext()) { UpdateTemplate template = new UpdateTemplate(resolver, solutions.next()); diff --git a/src/main/java/fr/inria/corese/core/next/storage/api/StorageManager.java b/src/main/java/fr/inria/corese/core/next/storage/api/StorageManager.java index ebd03494d..f20d29d1f 100644 --- a/src/main/java/fr/inria/corese/core/next/storage/api/StorageManager.java +++ b/src/main/java/fr/inria/corese/core/next/storage/api/StorageManager.java @@ -28,6 +28,22 @@ default StorageManager openSession() { return this; } + /** + * Captures an independent, read-only session over the currently visible data, + * including this session's uncommitted writes. Later writes, commits and rollbacks + * must not alter the captured view. The caller must close it to release snapshots. + * The returned session belongs to the calling thread; closing it never commits + * or rolls back the source session's transaction. Reads are confined to the + * creating thread, but close must be idempotent and safe from a cancellation + * thread. It must not shut down the shared database. + * + * @return an owned read snapshot + * @throws UnsupportedOperationException if independent read snapshots are unavailable + */ + default StorageManager openReadSnapshot() { + throw new UnsupportedOperationException("Independent read snapshots are unavailable"); + } + /** @return statement query operations */ QueryOperations queries(); diff --git a/src/main/java/fr/inria/corese/core/next/storage/api/exception/StorageException.java b/src/main/java/fr/inria/corese/core/next/storage/api/exception/StorageException.java index 94890640f..aae64617b 100644 --- a/src/main/java/fr/inria/corese/core/next/storage/api/exception/StorageException.java +++ b/src/main/java/fr/inria/corese/core/next/storage/api/exception/StorageException.java @@ -8,6 +8,9 @@ * Specific exception for StorageManager operations. */ public class StorageException extends CoreseException { + @java.io.Serial + private static final long serialVersionUID = 1L; + private final ErrorCode code; diff --git a/src/main/java/fr/inria/corese/core/next/storage/api/operations/QueryOperations.java b/src/main/java/fr/inria/corese/core/next/storage/api/operations/QueryOperations.java index 92520fd2f..5a9dd62d1 100644 --- a/src/main/java/fr/inria/corese/core/next/storage/api/operations/QueryOperations.java +++ b/src/main/java/fr/inria/corese/core/next/storage/api/operations/QueryOperations.java @@ -11,6 +11,25 @@ */ public interface QueryOperations { + /** + * Returns a token identifying the data currently visible to this session. + * A supported token is stable across reads and changes after effective content + * or graph-registry mutations. A snapshot transaction keeps its captured token + * despite other sessions' commits, and changes it after its own writes. After + * rollback the token identifies the restored visible state. Tokens are opaque: + * clients compare equality, never infer statement counts or commit order. + * + *

    Backends unable to provide reliable invalidation return {@code -1}; query + * planning then remains uncached. Implementations must validate session access + * and must not create a read cursor or transaction to obtain this token.

    + * + * @return non-negative session-visible data token, or {@code -1} if unavailable + * @throws StorageException if the visible version cannot be read + */ + default long getDataVersion() { + return -1; + } + /** * Queries statements matching the given pattern. * diff --git a/src/main/java/fr/inria/corese/core/next/storage/api/plugin/PluginException.java b/src/main/java/fr/inria/corese/core/next/storage/api/plugin/PluginException.java index f2abbeeb9..012195d7d 100644 --- a/src/main/java/fr/inria/corese/core/next/storage/api/plugin/PluginException.java +++ b/src/main/java/fr/inria/corese/core/next/storage/api/plugin/PluginException.java @@ -8,6 +8,9 @@ */ @SuppressWarnings("java:S110") public class PluginException extends StorageException { + @java.io.Serial + private static final long serialVersionUID = 1L; + public PluginException(String message) { super(ErrorCode.PLUGIN_FAILED, message); diff --git a/src/main/java/fr/inria/corese/core/next/storage/api/plugin/PluginNotFoundException.java b/src/main/java/fr/inria/corese/core/next/storage/api/plugin/PluginNotFoundException.java index d6184678d..fdc94ce8c 100644 --- a/src/main/java/fr/inria/corese/core/next/storage/api/plugin/PluginNotFoundException.java +++ b/src/main/java/fr/inria/corese/core/next/storage/api/plugin/PluginNotFoundException.java @@ -5,6 +5,9 @@ */ @SuppressWarnings("java:S110") public class PluginNotFoundException extends PluginException { + @java.io.Serial + private static final long serialVersionUID = 1L; + public PluginNotFoundException(String message) { super(message); 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 1719dae35..5ade691f6 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 @@ -50,6 +50,7 @@ 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; @@ -80,6 +81,9 @@ */ final class IndexedStatementStore implements QueryOperations, MutationOperations, MetadataOperations, TransactionManager { + + /** Allocates unique content versions without adding fields to retained database states. */ + private static final AtomicLong REVISIONS = new AtomicLong(); // One batch-wide reservation: at most 65,536 HashMap buckets, even for many graphs. /** * Upper bound on the batch-wide initial canonicalization reservation. @@ -520,6 +524,23 @@ IndexedStatementStore openSession() { } } + /** Captures the visible state without transferring ownership of an active transaction. */ + @SuppressWarnings("java:S1181") // Release retained partitions if snapshot initialization fails, then rethrow unchanged. + IndexedStatementStore openReadSnapshot() { + synchronized (database) { + checkAccess(); + final IndexedStatementStore snapshot = new IndexedStatementStore(database); + final State captured = state().copy(); + try { + snapshot.transaction = snapshot.new SnapshotTransaction(captured, getDataVersion(), true); + return snapshot; + } catch (final RuntimeException | Error failure) { + captured.release(); + throw failure; + } + } + } + /** * Closes this session, rolling back its active transaction; independent read snapshots remain retained. * @@ -534,7 +555,11 @@ void closeSession() { throw new IllegalStateException("A batch is active on this session"); } if (transaction != null) { - transaction.rollback(); + if (transaction.readOnly) { + transaction.finish(TransactionState.ROLLED_BACK); + } else { + transaction.rollback(); + } } closed = true; } @@ -561,6 +586,9 @@ private void checkAccess() { */ private void checkWrite() { checkAccess(); + if (transaction != null && transaction.readOnly) { + throw new IllegalStateException("Read snapshots cannot be mutated"); + } if (loading) { throw new IllegalStateException("A batch is active on this session"); } @@ -585,9 +613,19 @@ private void changed(final boolean changed) { return; } if (transaction == null) { - database.version++; + database.version = REVISIONS.incrementAndGet(); } else { transaction.dirty = true; + transaction.revision = REVISIONS.incrementAndGet(); + } + } + + /** Returns the version of this session's visible data under the database monitor. */ + @Override + public long getDataVersion() { + synchronized (database) { + checkAccess(); + return transaction == null ? database.version : transaction.revision; } } @@ -1656,6 +1694,10 @@ private final class SnapshotTransaction implements Transaction { * 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. */ @@ -1676,8 +1718,14 @@ private final class SnapshotTransaction implements Transaction { * @param version committed version captured for conflict detection */ private SnapshotTransaction(final State state, final long version) { + this(state, version, false); + } + + private SnapshotTransaction(final State state, final long version, final boolean readOnly) { this.state = state; this.version = version; + this.revision = version; + this.readOnly = readOnly; } /** @@ -1758,7 +1806,7 @@ public void commit() { database.committed.release(); database.committed = state; state = null; // Ownership transferred to the committed state. - database.version++; + database.version = revision; } finish(TransactionState.COMMITTED); } diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/MemoryStorageManager.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/MemoryStorageManager.java index a55d2c001..b6124b25e 100644 --- a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/MemoryStorageManager.java +++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/MemoryStorageManager.java @@ -85,6 +85,12 @@ public StorageManager openSession() { return new MemoryStorageManager(store.openSession()); } + /** {@inheritDoc} */ + @Override + public StorageManager openReadSnapshot() { + return new MemoryStorageManager(store.openReadSnapshot()); + } + /** * {@inheritDoc} * diff --git a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/plugin/MemoryStoragePlugin.java b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/plugin/MemoryStoragePlugin.java index 5eb14193b..6e7dbcf26 100644 --- a/src/main/java/fr/inria/corese/core/next/storage/impl/memory/plugin/MemoryStoragePlugin.java +++ b/src/main/java/fr/inria/corese/core/next/storage/impl/memory/plugin/MemoryStoragePlugin.java @@ -41,7 +41,9 @@ public String getDescription() { */ @Override public boolean supports(final StorageConfig config) { - return config != null && config.getType().map(getName()::equalsIgnoreCase).orElse(false); + return config != null && config.getType() + .map(t -> getName().equalsIgnoreCase(t) || "indexed-memory".equalsIgnoreCase(t)) + .orElse(false); } /** 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 afd2b0211..e5686be1a 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 @@ -24,16 +24,29 @@ class NextModuleBoundaryTest { private static final Path CORE_SOURCES = Path.of("src/main/java/fr/inria/corese/core"); private static final Path NEXT_SOURCES = CORE_SOURCES.resolve("next"); private static final Pattern CORESE_TYPE_REFERENCE = Pattern.compile( - "\\bfr\\.inria\\.corese\\.core(?:\\.[A-Za-z_$][A-Za-z0-9_$]*)+"); - private static final Pattern STATIC_IMPORT_PREFIX = Pattern.compile("^static\\s+"); + "\\bfr\\.inria\\.corese(?:\\.[A-Za-z_$][A-Za-z0-9_$]*+)++"); - // Engine must not depend on parser or AST types: zero exceptions. - // Exact source/type pairs only: see docs/next-engine-ast-boundary.md. - private static final Set EXISTING_ENGINE_AST_DEPENDENCIES = Set.of(); + @Test + void declaredIsolationMatchesTheActualRuntimeClasspath() { + boolean historicalClassesExpected = !Boolean.getBoolean("corese.next.isolated"); + for (String type : List.of("fr.inria.corese.core.Graph", + "fr.inria.corese.core.kgram.core.Eval", + "fr.inria.corese.core.sparql.triple.parser.ASTQuery")) { + boolean present; + try { + Class.forName(type, false, getClass().getClassLoader()); + present = true; + } catch (ClassNotFoundException absent) { + present = false; + } + assertEquals(historicalClassesExpected, present, + "Historical class availability must match the declared verification mode: " + type); + } + } @Test void sharedCodeMustNotDependOnDomainModules() throws IOException { - assertNoImports( + assertNoReferences( NEXT_SOURCES.resolve("common"), imported -> imported.startsWith("fr.inria.corese.core.next.data.") || imported.startsWith("fr.inria.corese.core.next.storage.") @@ -42,7 +55,7 @@ void sharedCodeMustNotDependOnDomainModules() throws IOException { @Test void dataMustNotDependOnStorageOrQuery() throws IOException { - assertNoImports( + assertNoReferences( NEXT_SOURCES.resolve("data"), imported -> imported.startsWith("fr.inria.corese.core.next.storage.") || imported.startsWith("fr.inria.corese.core.next.query.")); @@ -50,11 +63,26 @@ void dataMustNotDependOnStorageOrQuery() throws IOException { @Test void storageMustNotDependOnQuery() throws IOException { - assertNoImports( + assertNoReferences( NEXT_SOURCES.resolve("storage"), imported -> imported.startsWith("fr.inria.corese.core.next.query.")); } + @Test + void nextMustNotDependOnAnyHistoricalCoresePackage() throws IOException { + assertNoReferences(NEXT_SOURCES, + reference -> reference.startsWith("fr.inria.corese.") + && !reference.equals("fr.inria.corese.core.next") + && !reference.startsWith("fr.inria.corese.core.next.")); + } + + @Test + void syntaxTreeMustNotDependOnItsParserOrExecutionEngine() throws IOException { + assertNoReferences(NEXT_SOURCES.resolve("query/impl/sparql/ast"), + reference -> reference.startsWith("fr.inria.corese.core.next.query.impl.sparql.parser.") + || reference.startsWith("fr.inria.corese.core.next.query.impl.engine.")); + } + @Test void publicContractsMustNotDependOnImplementations() throws IOException { for (Path apiDirectory : publicContractDirectories()) { @@ -96,7 +124,7 @@ void queryRuntimeMustNotDependOnTheLegacyPipeline() throws IOException { @Test void queryEngineMustNotAddParserOrAstDependencies() throws IOException { Path engineSources = NEXT_SOURCES.resolve("query/impl/engine"); - assertEquals(EXISTING_ENGINE_AST_DEPENDENCIES, engineSyntaxDependencies(engineSources), + assertEquals(Set.of(), engineSyntaxDependencies(engineSources), "Unexpected engine syntax dependency, or obsolete exception: " + "remove dependencies rather than expanding the exception list."); } @@ -199,9 +227,9 @@ void publicApiAndSpiPackagesMustBeDocumented() throws IOException { } @Test - void implementationPackagesMustBeDocumented() throws IOException { + void allNativePackagesMustBeDocumented() throws IOException { assertPackagesAreDocumented( - implementationDirectories(), "Undocumented implementation packages:"); + List.of(NEXT_SOURCES), "Undocumented native packages:"); } private static void assertPackagesAreDocumented(List packageRoots, String message) @@ -234,48 +262,6 @@ private static List publicContractDirectories() { NEXT_SOURCES.resolve("query/api")); } - private static List implementationDirectories() { - return List.of( - NEXT_SOURCES.resolve("data/impl"), - NEXT_SOURCES.resolve("storage/impl"), - NEXT_SOURCES.resolve("query/impl/engine"), - NEXT_SOURCES.resolve("query/impl/sparql")); - } - - private static void assertNoImports(Path sourceDirectory, Predicate forbidden) - throws IOException { - assertNoImports(sourceDirectory, source -> true, forbidden); - } - - private static void assertNoImports( - Path sourceDirectory, - Predicate includedSource, - Predicate forbidden) throws IOException { - List violations = new ArrayList<>(); - try (Stream paths = Files.walk(sourceDirectory)) { - for (Path source : paths - .filter(path -> path.toString().endsWith(".java")) - .filter(includedSource) - .toList()) { - for (String line : Files.readAllLines(source)) { - String trimmed = line.trim(); - if (trimmed.startsWith("import ")) { - String imported = STATIC_IMPORT_PREFIX - .matcher(trimmed.substring("import ".length())) - .replaceFirst("") - .replace(";", ""); - if (forbidden.test(imported)) { - violations.add(NEXT_SOURCES.relativize(source) + " -> " + imported); - } - } - } - } - } - if (!violations.isEmpty()) { - fail("Forbidden next-module dependencies:\n" + String.join("\n", violations)); - } - } - private static void assertNoReferences( Path sourceDirectory, Predicate forbidden) throws IOException { diff --git a/src/test/java/fr/inria/corese/core/next/data/api/model/RdfValueOrderTest.java b/src/test/java/fr/inria/corese/core/next/data/api/model/RdfValueOrderTest.java index 34d65c878..e2ce0018e 100644 --- a/src/test/java/fr/inria/corese/core/next/data/api/model/RdfValueOrderTest.java +++ b/src/test/java/fr/inria/corese/core/next/data/api/model/RdfValueOrderTest.java @@ -4,9 +4,12 @@ import fr.inria.corese.core.next.data.api.factory.ValueFactory; import fr.inria.corese.core.next.data.api.literal.XSDDatatype; import fr.inria.corese.core.next.data.api.term.Literal; +import fr.inria.corese.core.next.data.api.term.SimpleLiteral; import fr.inria.corese.core.next.data.api.term.Value; import fr.inria.corese.core.next.data.api.vocabulary.XSD; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import java.math.BigDecimal; @@ -46,6 +49,21 @@ void comparesNumericLiteralsByValueBeforeLexicalForm() { assertTrue(two.compare(ten) < 0); } + @ParameterizedTest + @EnumSource(value = XSDDatatype.class, names = {"INTEGER", "DECIMAL", "FLOAT", "DOUBLE"}) + void genericTypedNumbersUseTheSameOrderingWithoutChangingTheirLexicalForm(XSDDatatype datatype) { + var two = new SimpleLiteral("02", datatype.getIRI()); + var ten = new SimpleLiteral("10", datatype.getIRI()); + Literal specializedTwo = values.createLiteral("02", datatype.getIRI()); + Literal specializedTen = values.createLiteral("10", datatype.getIRI()); + assertTrue(two.compare(ten) < 0); + assertTrue(ten.compare(two) > 0); + assertEquals(0, two.compare(specializedTwo)); + assertEquals(Integer.signum(specializedTen.compare(two)), Integer.signum(ten.compare(specializedTwo))); + assertEquals("02", two.getLabel()); + assertEquals(datatype.getIRI(), two.getDatatype()); + } + @Test void usesRdfTermTieBreakersForEqualNumericValues() { DatatypeValue integer = values.createLiteral("1", XSD.xsdInteger.getIRI()); diff --git a/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsListenerTest.java b/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsListenerTest.java index 3e709c436..425d0791b 100644 --- a/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsListenerTest.java +++ b/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsListenerTest.java @@ -3,7 +3,7 @@ import fr.inria.corese.core.next.data.api.term.*; import fr.inria.corese.core.next.data.api.model.*; import fr.inria.corese.core.next.data.api.factory.ValueFactory; -import fr.inria.corese.core.next.data.api.io.option.IOOptions; +import fr.inria.corese.core.next.data.impl.io.parser.support.NTriplesQuadsTermDecoder; import fr.inria.corese.core.next.data.api.exception.ParsingException; import fr.inria.corese.core.next.generated.antlr.NQuadsParser; import org.antlr.v4.runtime.ParserRuleContext; @@ -34,12 +34,10 @@ class NQuadsListenerTest { @Mock private ValueFactory mockValueFactory; - @Mock - private IOOptions mockIOOptions; + private NTriplesQuadsTermDecoder termDecoder; private NQuadsListener listener; - @Mock private IRI mockIRI; @Mock @@ -55,7 +53,8 @@ class NQuadsListenerTest { @BeforeEach void setUp() { - listener = new NQuadsListener(mockModel, mockValueFactory, mockIOOptions); + termDecoder = new NTriplesQuadsTermDecoder(mockValueFactory); + listener = new NQuadsListener(mockModel, mockValueFactory); lenient().when(mockValueFactory.createIRI(anyString())).thenAnswer(invocation -> { String uri = invocation.getArgument(0); @@ -106,7 +105,6 @@ void testEnterExitStatementAddsToModelWithIRIGraph() { NQuadsParser.ObjectContext objectCtx = mockRuleContext(NQuadsParser.ObjectContext.class); NQuadsParser.GraphLabelContext graphLabelCtx = mockRuleContext(NQuadsParser.GraphLabelContext.class); - TerminalNode subjectIriRef = mockTerminalNode(""); when(subjectCtx.IRIREF()).thenReturn(subjectIriRef); @@ -119,7 +117,6 @@ void testEnterExitStatementAddsToModelWithIRIGraph() { TerminalNode graphIriRef = mockTerminalNode(""); when(graphLabelCtx.IRIREF()).thenReturn(graphIriRef); - when(statementCtx.subject()).thenReturn(subjectCtx); when(statementCtx.predicate()).thenReturn(predicateCtx); when(statementCtx.object()).thenReturn(objectCtx); @@ -134,7 +131,6 @@ void testEnterExitStatementAddsToModelWithIRIGraph() { when(mockValueFactory.createIRI("http://example.org/object")).thenReturn(actualObjectIRI); when(mockValueFactory.createIRI("http://example.org/graph")).thenReturn(actualGraphIRI); - listener.enterStatement(statementCtx); listener.exitStatement(statementCtx); @@ -171,7 +167,6 @@ void testEnterExitStatementAddsToModelDefaultGraph() { when(mockValueFactory.createIRI("http://example.org/predicate")).thenReturn(actualPredicateIRI); when(mockValueFactory.createIRI("http://example.org/object")).thenReturn(actualObjectIRI); - listener.enterStatement(statementCtx); listener.exitStatement(statementCtx); @@ -277,7 +272,6 @@ void testExtractObjectLiteralLang() { verify(mockValueFactory).createLiteral("hello", "en"); } - @Test @DisplayName("extractGraph should return IRI for IRIREF context") void testExtractGraphIRI() { @@ -302,64 +296,47 @@ void testExtractGraphBNode() { verify(mockValueFactory).createBNode("graph1"); } - - - @Test - @DisplayName("unescapeLiteral should throw IllegalArgumentException for invalid \\UXXXXXXXX") - @SuppressWarnings("deprecation") - void testUnescapeLiteralInvalidUx() throws NoSuchMethodException { + @DisplayName("unescapeLiteral should throw ParsingException for invalid \\UXXXXXXXX") + void testUnescapeLiteralInvalidUx() { String input = "\"Invalid\\U0000XXX\""; - java.lang.reflect.Method method = NQuadsListener.class.getDeclaredMethod("unescapeLiteral", String.class); - method.setAccessible(true); assertThrows(ParsingException.class, - () -> listener.unescapeLiteral(input), + () -> termDecoder.unescapeLiteral(input), "Should throw for malformed \\UXXXXXXXX escape sequence"); } - @Test @DisplayName("unescapeUri should handle basic escape sequences") - void testUnescapeUriBasicEscapes() throws NoSuchMethodException, java.lang.reflect.InvocationTargetException, IllegalAccessException { + void testUnescapeUriBasicEscapes() { String input = "http://example.org/path\\>with\\ listener.unescapeLiteral(input), - "Should throw unescapeUri should throw IllegalArgumentException for invalid \\uXXXX"); + () -> termDecoder.unescapeUri(input), + "Malformed Unicode escapes in IRIs must fail"); } - } diff --git a/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesListenerTest.java b/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesListenerTest.java index 497af9420..12ab2783c 100644 --- a/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesListenerTest.java +++ b/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesListenerTest.java @@ -3,7 +3,7 @@ import fr.inria.corese.core.next.data.api.term.*; import fr.inria.corese.core.next.data.api.model.*; import fr.inria.corese.core.next.data.api.factory.ValueFactory; -import fr.inria.corese.core.next.data.api.io.option.IOOptions; +import fr.inria.corese.core.next.data.impl.io.parser.support.NTriplesQuadsTermDecoder; import fr.inria.corese.core.next.data.api.exception.ParsingException; import fr.inria.corese.core.next.generated.antlr.NTriplesParser; import org.antlr.v4.runtime.ParserRuleContext; @@ -20,7 +20,7 @@ import static org.mockito.Mockito.*; /** - * Unit tests for the NTriplesListenerImpl class. + * Unit tests for the NTriplesListener class. * These tests verify that the listener correctly processes ANTLR parse tree contexts * to extract and unescape RDF terms (IRIs, Blank Nodes, Literals) and add them to the model. */ @@ -33,8 +33,7 @@ class NTriplesListenerTest { @Mock private ValueFactory mockValueFactory; - @Mock - private IOOptions mockIOOptions; + private NTriplesQuadsTermDecoder termDecoder; private NTriplesListener listener; @@ -49,7 +48,8 @@ class NTriplesListenerTest { @BeforeEach void setUp() { - listener = new NTriplesListener(mockModel, mockValueFactory, mockIOOptions); + termDecoder = new NTriplesQuadsTermDecoder(mockValueFactory); + listener = new NTriplesListener(mockModel, mockValueFactory); lenient().when(mockValueFactory.createIRI(anyString())).thenAnswer(invocation -> { String uri = invocation.getArgument(0); @@ -117,7 +117,6 @@ void testEnterExitTripleAddsToModel() { when(mockValueFactory.createIRI("http://example.org/predicate")).thenReturn(actualPredicateIRI); when(mockValueFactory.createIRI("http://example.org/object")).thenReturn(actualObjectIRI); - listener.enterTriple(tripleCtx); listener.exitTriple(tripleCtx); @@ -223,63 +222,47 @@ void testExtractObjectLiteralLang() { verify(mockValueFactory).createLiteral("hello", "en"); } - @Test @DisplayName("unescapeLiteral should throw ParsingException for invalid \\uXXXX") - @SuppressWarnings("deprecation") - void testUnescapeLiteralInvalidUx() throws NoSuchMethodException { + void testUnescapeLiteralInvalidUx() { String input = "\"Invalid\\uXXXX\""; - java.lang.reflect.Method method = NTriplesListener.class.getDeclaredMethod("unescapeLiteral", String.class); - method.setAccessible(true); - assertThrows(ParsingException.class, - () -> listener.unescapeLiteral(input), - "Should throw unescapeLiteral should throw ParsingException for invalid \\uXXXX"); + () -> termDecoder.unescapeLiteral(input), + "Malformed Unicode escapes must fail"); } @Test @DisplayName("unescapeLiteral should throw ParsingException for invalid \\UXXXXXXXX") - @SuppressWarnings("deprecation") - void testUnescapeLiteralInvalid() throws NoSuchMethodException { + void testUnescapeLiteralInvalid() { String input = "\"Invalid\\U0000XXX\""; - java.lang.reflect.Method method = NTriplesListener.class.getDeclaredMethod("unescapeLiteral", String.class); - method.setAccessible(true); - assertThrows(ParsingException.class, - () -> listener.unescapeLiteral(input), - "Should throw unescapeLiteral should throw ParsingException for invalid \\UXXXXXXXX"); + () -> termDecoder.unescapeLiteral(input), + "Malformed Unicode escapes must fail"); } - @Test @DisplayName("unescapeUri should handle basic escape sequences") - void testUnescapeUriBasicEscapes() throws NoSuchMethodException, java.lang.reflect.InvocationTargetException, IllegalAccessException { + void testUnescapeUriBasicEscapes() { String input = "http://example.org/path\\>with\\ <%s> \"%s\"", mockExPerson.stringValue(), mockExName.stringValue(), - escapeNQuadsString(lexJohn)) + " .\n"; + escapeNQuadsString(LEX_JOHN)) + " .\n"; assertEquals(expected, writer.toString()); } @@ -102,10 +103,8 @@ void writeShouldHandleBlankNodes() throws SerializationException { StringWriter writer = new StringWriter(); nQuadsSerializer.write(writer); - String expected = String.format("_:%s <%s> _:%s", - mockBNode1.stringValue(), - mockExKnows.stringValue(), - mockBNode2.stringValue()) + " .\n"; + String expected = "_:" + mockBNode1.stringValue() + " <" + mockExKnows.stringValue() + + "> _:" + mockBNode2.stringValue() + " .\n"; assertEquals(expected, writer.toString()); } @@ -214,7 +213,7 @@ void writeShouldHandleNullContext() throws SerializationException { String expected = String.format("<%s> <%s> \"%s\"", mockExPerson.stringValue(), mockExName.stringValue(), - escapeNQuadsString(lexJohn)) + " .\n"; + escapeNQuadsString(LEX_JOHN)) + " .\n"; assertEquals(expected, writer.toString()); } @@ -272,7 +271,7 @@ void shouldHandleLiteralsWithLanguageTags() throws SerializationException { String expectedOutput = String.format("<%s> <%s> \"%s\"@%s", mockExPerson.stringValue(), factory.createIRI("http://example.org/greeting").stringValue(), - escapeNQuadsString(hello), + escapeNQuadsString(HELLO), languageTag) + " .\n"; assertEquals(expectedOutput, writer.toString()); @@ -358,6 +357,9 @@ public boolean hasNext() { @Override public Statement next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } return statements[index++]; } } diff --git a/src/test/java/fr/inria/corese/core/next/data/impl/io/serializer/ntriples/NTriplesSerializerTest.java b/src/test/java/fr/inria/corese/core/next/data/impl/io/serializer/ntriples/NTriplesSerializerTest.java index dd76586a2..4a793d55b 100644 --- a/src/test/java/fr/inria/corese/core/next/data/impl/io/serializer/ntriples/NTriplesSerializerTest.java +++ b/src/test/java/fr/inria/corese/core/next/data/impl/io/serializer/ntriples/NTriplesSerializerTest.java @@ -17,6 +17,7 @@ import java.io.StringWriter; import java.io.Writer; import java.util.Iterator; +import java.util.NoSuchElementException; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -24,6 +25,9 @@ class NTriplesSerializerTest { + private static final String LEX_JOHN = "John Doe"; + private static final String HELLO = "Hello"; + private Model model; private NTriplesSerializer nTriplesSerializer; private MockStatementFactory factory; @@ -32,9 +36,6 @@ class NTriplesSerializerTest { private IRI mockExName; private IRI mockExKnows; - private final String lexJohn = "John Doe"; - private final String hello = "Hello"; - private Literal mockLiteralJohn; private Literal mockLiteralHelloEn; private Resource mockBNode1; @@ -51,8 +52,8 @@ void setUp() { mockExName = factory.createIRI("http://example.org/name"); mockExKnows = factory.createIRI("http://example.org/knows"); - mockLiteralJohn = factory.createLiteral(lexJohn, null, null); - mockLiteralHelloEn = factory.createLiteral(hello, null, "en"); + mockLiteralJohn = factory.createLiteral(LEX_JOHN, null, null); + mockLiteralHelloEn = factory.createLiteral(HELLO, null, "en"); mockBNode1 = factory.createBlankNode("b1"); mockBNode2 = factory.createBlankNode("b2"); @@ -86,7 +87,7 @@ void writeShouldSerializeSimpleStatement() throws SerializationException { String expected = String.format("<%s> <%s> \"%s\"", mockExPerson.stringValue(), mockExName.stringValue(), - escapeNTriplesString(lexJohn)) + " .\n"; + escapeNTriplesString(LEX_JOHN)) + " .\n"; assertEquals(expected, writer.toString()); } @@ -109,7 +110,7 @@ void writeShouldSerializeStatementWithContext() throws SerializationException { String expected = String.format("<%s> <%s> \"%s\"", mockExPerson.stringValue(), mockExName.stringValue(), - escapeNTriplesString(lexJohn)) + " .\n"; + escapeNTriplesString(LEX_JOHN)) + " .\n"; assertEquals(expected, writer.toString()); } @@ -127,10 +128,8 @@ void writeShouldHandleBlankNodes() throws SerializationException { StringWriter writer = new StringWriter(); nTriplesSerializer.write(writer); - String expected = String.format("_:%s <%s> _:%s", - mockBNode1.stringValue(), - mockExKnows.stringValue(), - mockBNode2.stringValue()) + " .\n"; + String expected = "_:" + mockBNode1.stringValue() + " <" + mockExKnows.stringValue() + + "> _:" + mockBNode2.stringValue() + " .\n"; assertEquals(expected, writer.toString()); } @@ -221,7 +220,7 @@ void writeShouldHandleNullContext() throws SerializationException { String expected = String.format("<%s> <%s> \"%s\"", mockExPerson.stringValue(), mockExName.stringValue(), - escapeNTriplesString(lexJohn)) + " .\n"; + escapeNTriplesString(LEX_JOHN)) + " .\n"; assertEquals(expected, writer.toString()); } @@ -280,7 +279,7 @@ void shouldHandleLiteralsWithLanguageTags() throws SerializationException { String expectedOutput = String.format("<%s> <%s> \"%s\"@%s", mockExPerson.stringValue(), factory.createIRI("http://example.org/greeting").stringValue(), - escapeNTriplesString(hello), + escapeNTriplesString(HELLO), languageTag) + " .\n"; assertEquals(expectedOutput, writer.toString()); @@ -365,6 +364,9 @@ public boolean hasNext() { @Override public Statement next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } return statements[index++]; } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/ExecutionDeliveryTimingTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/ExecutionDeliveryTimingTest.java new file mode 100644 index 000000000..fa1ef0e92 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/ExecutionDeliveryTimingTest.java @@ -0,0 +1,116 @@ +package fr.inria.corese.core.next.query.impl.engine.diagnostics; + +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.ExecutionMode; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.FallbackReason; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.FirstRowStatus; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.ResultDelivery; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.jupiter.api.Assertions.*; + +/** Deterministic timing contracts: client pauses are not query execution time. */ +class ExecutionDeliveryTimingTest { + @Test + void includesInitialWorkAndFirstAdvanceButExcludesClientIdleTime() { + var time = new AtomicLong(10); + var diagnostics = new ExecutionDiagnostics(time::get); + diagnostics.selectDelivery(FallbackReason.NONE); + diagnostics.beginWork(); + time.addAndGet(100); + assertEquals(100, diagnostics.endWork()); + var before = diagnostics.snapshot(); + + time.addAndGet(10_000); + diagnostics.beginWork(); + time.addAndGet(40); + diagnostics.firstRow(true); + time.addAndGet(60); + assertEquals(100, diagnostics.endWork()); + + assertEquals(140, diagnostics.snapshot().delivery().firstRowNanos()); + assertEquals(FirstRowStatus.UNOBSERVED, before.delivery().firstRowStatus()); + assertEquals(-1, before.delivery().firstRowNanos()); + time.addAndGet(50_000); + diagnostics.beginWork(); + time.addAndGet(80); + diagnostics.firstRow(false); + diagnostics.endWork(); + assertEquals(140, diagnostics.snapshot().delivery().firstRowNanos()); + assertEquals(FirstRowStatus.AVAILABLE, diagnostics.snapshot().delivery().firstRowStatus()); + } + + @Test + void nestedCompilationOrCleanupScopesDoNotCountWorkTwice() { + var time = new AtomicLong(); + var diagnostics = new ExecutionDiagnostics(time::get); + diagnostics.selectDelivery(FallbackReason.NONE); + diagnostics.beginWork(); + time.addAndGet(10); + diagnostics.beginWork(); + time.addAndGet(20); + assertEquals(0, diagnostics.endWork()); + time.addAndGet(10); + diagnostics.firstRow(true); + assertEquals(40, diagnostics.endWork()); + assertEquals(40, diagnostics.snapshot().delivery().firstRowNanos()); + assertThrows(IllegalStateException.class, diagnostics::endWork); + } + + @Test + void emptyResultsHaveAnExplicitStatusAndNoInventedFirstRowTime() { + var time = new AtomicLong(); + var diagnostics = new ExecutionDiagnostics(time::get); + diagnostics.selectDelivery(FallbackReason.ORDER_BY); + diagnostics.beginWork(); + time.addAndGet(100); + diagnostics.firstRow(false); + diagnostics.endWork(); + diagnostics.firstRow(true); + var delivery = diagnostics.snapshot().delivery(); + assertEquals(ExecutionMode.MATERIALIZED, delivery.mode()); + assertEquals(FallbackReason.ORDER_BY, delivery.fallbackReason()); + assertEquals(FirstRowStatus.EMPTY, delivery.firstRowStatus()); + assertEquals(-1, delivery.firstRowNanos()); + } + + @Test + void aNanotimeWrapDoesNotChangeAnElapsedDuration() { + var time = new AtomicLong(Long.MAX_VALUE - 20); + var diagnostics = new ExecutionDiagnostics(time::get); + diagnostics.selectDelivery(FallbackReason.NONE); + diagnostics.beginWork(); + time.addAndGet(40); + diagnostics.firstRow(true); + diagnostics.endWork(); + assertEquals(40, diagnostics.snapshot().delivery().firstRowNanos()); + } + + @Test + void nonSelectFormsHaveNoFirstRowMetric() { + var diagnostics = new ExecutionDiagnostics(); + diagnostics.firstRow(true); + var delivery = diagnostics.snapshot().delivery(); + assertEquals(FallbackReason.QUERY_FORM, delivery.fallbackReason()); + assertEquals(FirstRowStatus.NOT_APPLICABLE, delivery.firstRowStatus()); + assertEquals(-1, delivery.firstRowNanos()); + assertThrows(IllegalArgumentException.class, () -> diagnostics.selectDelivery(FallbackReason.QUERY_FORM)); + } + + @Test + void deliveryCannotClaimStreamingWithAMaterializationReasonOrATimeWithoutARow() { + assertThrows(IllegalArgumentException.class, () -> new ResultDelivery(ExecutionMode.STREAMING, + FallbackReason.ORDER_BY, FirstRowStatus.UNOBSERVED, -1)); + assertThrows(IllegalArgumentException.class, () -> new ResultDelivery(ExecutionMode.MATERIALIZED, + FallbackReason.NONE, FirstRowStatus.UNOBSERVED, -1)); + assertThrows(IllegalArgumentException.class, () -> new ResultDelivery(ExecutionMode.STREAMING, + FallbackReason.NONE, FirstRowStatus.AVAILABLE, -1)); + assertThrows(IllegalArgumentException.class, () -> new ResultDelivery(ExecutionMode.STREAMING, + FallbackReason.NONE, FirstRowStatus.EMPTY, 0)); + assertThrows(IllegalArgumentException.class, () -> new ResultDelivery(ExecutionMode.STREAMING, + FallbackReason.NONE, FirstRowStatus.NOT_APPLICABLE, -1)); + assertThrows(IllegalArgumentException.class, () -> new ResultDelivery(ExecutionMode.MATERIALIZED, + FallbackReason.QUERY_FORM, FirstRowStatus.AVAILABLE, 20)); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/QueryDeliveryDiagnosticsTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/QueryDeliveryDiagnosticsTest.java new file mode 100644 index 000000000..dac93de70 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/QueryDeliveryDiagnosticsTest.java @@ -0,0 +1,306 @@ +package fr.inria.corese.core.next.query.impl.engine.diagnostics; + +import fr.inria.corese.core.next.query.Repositories; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.ExecutionMode; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.FallbackReason; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.FirstRowStatus; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.repository.Repository; +import fr.inria.corese.core.next.query.impl.engine.eval.SelectCursor; +import fr.inria.corese.core.next.query.impl.engine.model.ExpType.Type; +import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; +import fr.inria.corese.core.next.query.impl.sparql.execution.SparqlQueryExecutor; +import fr.inria.corese.core.next.storage.api.StorageManager; +import fr.inria.corese.core.next.storage.api.operations.QueryOperations; +import fr.inria.corese.core.next.storage.impl.memory.MemoryStorageManager; +import fr.inria.corese.core.next.storage.impl.memory.StorageBenchmark; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.stream.Stream; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class QueryDeliveryDiagnosticsTest { + private static final String PREFIX = "PREFIX : "; + private MemoryStorageManager storage; + private Repository repository; + + @BeforeEach + void setUp() { + storage = MemoryStorageManager.builder().build(); + storage.mutations().addAll(StorageBenchmark.data(20, 0, false)); + repository = Repositories.create(storage); + } + + @AfterEach + void tearDown() { + repository.close(); + } + + @Test + void reportsActualStreamingAndFreezesTheTimeWhenTheFirstRowBecomesReady() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }").evaluateWithDiagnostics(); + var before = execution.profile(); + assertEquals(ExecutionMode.STREAMING, before.delivery().mode()); + assertEquals(FallbackReason.NONE, before.delivery().fallbackReason()); + assertEquals(FirstRowStatus.UNOBSERVED, before.delivery().firstRowStatus()); + assertEquals(-1, before.delivery().firstRowNanos()); + assertEquals(0, before.patterns().getFirst().candidateEdges()); + try (var rows = execution.result()) { + assertTrue(rows.hasNext()); + var first = execution.profile().delivery(); + assertEquals(FirstRowStatus.AVAILABLE, first.firstRowStatus()); + assertTrue(first.firstRowNanos() > 0); + assertEquals(1, execution.profile().patterns().getFirst().candidateEdges()); + assertNotNull(rows.next()); + assertEquals(119, rows.stream().count()); + assertEquals(first, execution.profile().delivery()); + } + assertEquals(FirstRowStatus.UNOBSERVED, before.delivery().firstRowStatus()); + assertTrue(execution.profile().format().contains("Delivery: STREAMING; fallback: NONE; first row: AVAILABLE")); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT * { ?p ?o }", + "SELECT * { ?s ?p ?o } LIMIT 0", + "SELECT * { ?s ?p ?o } OFFSET 120" + }) + void successfulExhaustionDistinguishesEmptyResultsFromUnobservedResults(String query) { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery(query).evaluateWithDiagnostics(); + assertEquals(FirstRowStatus.UNOBSERVED, execution.profile().delivery().firstRowStatus()); + try (var rows = execution.result()) { + assertFalse(rows.hasNext()); + } + assertEquals(FirstRowStatus.EMPTY, execution.profile().delivery().firstRowStatus()); + assertEquals(-1, execution.profile().delivery().firstRowNanos()); + } + } + + @Test + void closeBeforeTheFirstAdvanceDoesNotInventAnEmptyResult() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }").evaluateWithDiagnostics(); + execution.result().close(); + assertEquals(FirstRowStatus.UNOBSERVED, execution.profile().delivery().firstRowStatus()); + assertEquals(-1, execution.profile().delivery().firstRowNanos()); + assertEquals(0, execution.profile().patterns().getFirst().iteratorOpenings()); + } + } + + @Test + void anAvailableFirstRowRemainsRecordedAfterEarlyClose() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }").evaluateWithDiagnostics(); + long first; + try (var rows = execution.result()) { + assertNotNull(rows.next()); + first = execution.profile().delivery().firstRowNanos(); + } + assertEquals(FirstRowStatus.AVAILABLE, execution.profile().delivery().firstRowStatus()); + assertEquals(first, execution.profile().delivery().firstRowNanos()); + } + } + + @Test + void aReusedPlanRetainsTheDeliveryObservationWithoutRecompiling() { + try (var connection = repository.getConnection()) { + var prepared = connection.prepareTupleQuery("SELECT * { ?s ?p ?o } LIMIT 1"); + try (var first = prepared.evaluate()) { + assertTrue(first.hasNext()); + } + var execution = prepared.evaluateWithDiagnostics(); + try (var rows = execution.result()) { + assertTrue(execution.profile().planReused()); + assertEquals(0, execution.profile().compilationNanos()); + assertEquals(0, execution.profile().planningNanos()); + assertEquals(FirstRowStatus.UNOBSERVED, execution.profile().delivery().firstRowStatus()); + assertTrue(rows.hasNext()); + assertEquals(ExecutionMode.STREAMING, execution.profile().delivery().mode()); + assertTrue(execution.profile().delivery().firstRowNanos() > 0); + } + } + } + + @Test + void actualCursorTimingIncludesSnapshotSetupAndTraversalButExcludesClientPauses() { + var time = new AtomicLong(); + var diagnostics = new ExecutionDiagnostics(time::get); + StorageManager backend = mock(StorageManager.class, delegatesTo(storage)); + var snapshot = storage.openReadSnapshot(); + StorageManager view = mock(StorageManager.class, delegatesTo(snapshot)); + QueryOperations reads = mock(QueryOperations.class, delegatesTo(snapshot.queries())); + when(view.queries()).thenReturn(reads); + doAnswer(invocation -> { + time.addAndGet(11); + return view; + }).when(backend).openReadSnapshot(); + doAnswer(invocation -> { + time.addAndGet(7); + return snapshot.queries().find(invocation.getArgument(0)).peek(statement -> time.addAndGet(5)); + }).when(reads).find(any()); + try (var rows = new SparqlQueryExecutor(backend).withDiagnostics(diagnostics).evaluateTuple("SELECT * { ?s ?p ?o }")) { + time.addAndGet(10_000); + assertTrue(rows.hasNext()); + assertEquals(23, diagnostics.snapshot().delivery().firstRowNanos()); + time.addAndGet(20_000); + assertNotNull(rows.next()); + assertTrue(rows.hasNext()); + assertEquals(23, diagnostics.snapshot().delivery().firstRowNanos()); + } + verify(view, times(1)).close(); + } + + static Stream materializedQueries() { + return Stream.of( + Arguments.of("SELECT * { ?s ?p ?o } ORDER BY ?s LIMIT 1", FallbackReason.ORDER_BY), + Arguments.of("SELECT (COUNT(*) AS ?n) { ?s ?p ?o }", FallbackReason.AGGREGATION), + Arguments.of("SELECT ?p { ?s ?p ?o } GROUP BY ?p", FallbackReason.AGGREGATION), + Arguments.of("SELECT * { { SELECT ?s { ?s ?p ?o } LIMIT 1 } }", FallbackReason.SUBQUERY), + Arguments.of(PREFIX + "SELECT ?o { :s0 :link+ ?o }", FallbackReason.PROPERTY_PATH), + Arguments.of("SELECT ?s { ?s ?p ?o MINUS { ?s ?x } }", FallbackReason.MINUS), + Arguments.of("SELECT ?s ?copy { ?s ?p ?o OPTIONAL { BIND(?s AS ?copy) } } LIMIT 1", + FallbackReason.COMPLEX_OPTIONAL), + Arguments.of(PREFIX + "SELECT ?s ?name { { ?s :type :Person } { ?s :name ?name FILTER(BOUND(?name)) } }", + FallbackReason.SCOPED_JOIN), + Arguments.of("SELECT * { ?s ?p ?o GRAPH ?g { ?a ?b ?c FILTER(BOUND(?a)) } }", + FallbackReason.REPEATED_SCOPE) + ); + } + + @ParameterizedTest + @MethodSource("materializedQueries") + void explainsTheActualMaterializationGuard(String query, FallbackReason expected) { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery(query).evaluateWithDiagnostics(); + try (var rows = execution.result()) { + var delivery = execution.profile().delivery(); + assertEquals(ExecutionMode.MATERIALIZED, delivery.mode()); + assertEquals(expected, delivery.fallbackReason()); + assertEquals(rows.hasNext() ? FirstRowStatus.AVAILABLE : FirstRowStatus.EMPTY, delivery.firstRowStatus()); + if (rows.hasNext()) assertTrue(delivery.firstRowNanos() > 0); + else assertEquals(-1, delivery.firstRowNanos()); + } + } + } + + @Test + void aMaterializedEmptySelectIsKnownBeforeTheCallerStartsReading() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery("SELECT * { ?p ?o } ORDER BY ?p") + .evaluateWithDiagnostics(); + try (var rows = execution.result()) { + assertEquals(FirstRowStatus.EMPTY, execution.profile().delivery().firstRowStatus()); + assertEquals(-1, execution.profile().delivery().firstRowNanos()); + assertFalse(rows.hasNext()); + } + } + } + + @Test + void aggregateOverEmptyDataStillProducesARow() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery("SELECT (COUNT(*) AS ?n) { ?p ?o }") + .evaluateWithDiagnostics(); + try (var rows = execution.result()) { + assertEquals(FirstRowStatus.AVAILABLE, execution.profile().delivery().firstRowStatus()); + assertEquals("0", rows.next().getValue("n").stringValue()); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"ASK { ?s ?p ?o }", "ASK { ?p ?o }"}) + void askDoesNotConfuseABooleanResultWithASelectRow(String query) { + try (var connection = repository.getConnection()) { + var delivery = connection.prepareBooleanQuery(query).evaluateWithDiagnostics().profile().delivery(); + assertEquals(ExecutionMode.MATERIALIZED, delivery.mode()); + assertEquals(FallbackReason.QUERY_FORM, delivery.fallbackReason()); + assertEquals(FirstRowStatus.NOT_APPLICABLE, delivery.firstRowStatus()); + assertEquals(-1, delivery.firstRowNanos()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"CONSTRUCT { ?s ?o } { ?s ?p ?o } LIMIT 1", "DESCRIBE "}) + void graphResultsDoNotExposeASelectTiming(String query) { + try (var connection = repository.getConnection()) { + var execution = connection.prepareGraphQuery(query).evaluateWithDiagnostics(); + try (var rows = execution.result()) { + assertNotNull(rows); + assertEquals(FirstRowStatus.NOT_APPLICABLE, execution.profile().delivery().firstRowStatus()); + assertEquals(FallbackReason.QUERY_FORM, execution.profile().delivery().fallbackReason()); + } + } + } + + @Test + void backendWithoutIndependentSnapshotsReportsItsActualFallback() { + StorageManager backend = mock(StorageManager.class, delegatesTo(storage)); + doThrow(UnsupportedOperationException.class).when(backend).openReadSnapshot(); + var diagnostics = new ExecutionDiagnostics(); + var executor = new SparqlQueryExecutor(backend).withDiagnostics(diagnostics); + try (var rows = executor.evaluateTuple("SELECT * { ?s ?p ?o }")) { + assertTrue(rows.hasNext()); + var delivery = diagnostics.snapshot().delivery(); + assertEquals(ExecutionMode.MATERIALIZED, delivery.mode()); + assertEquals(FallbackReason.SNAPSHOT_UNAVAILABLE, delivery.fallbackReason()); + assertEquals(FirstRowStatus.AVAILABLE, delivery.firstRowStatus()); + } + } + + @Test + void queryGuardPrecedesBackendCapabilityWithoutExtraCountsOrSnapshotAttempts() { + StorageManager backend = mock(StorageManager.class, delegatesTo(storage)); + QueryOperations reads = mock(QueryOperations.class, delegatesTo(storage.queries())); + when(backend.queries()).thenReturn(reads); + var diagnostics = new ExecutionDiagnostics(); + var executor = new SparqlQueryExecutor(backend).withDiagnostics(diagnostics); + try (var rows = executor.evaluateTuple("SELECT * { ?s ?p ?o } ORDER BY ?s")) { + assertTrue(rows.hasNext()); + assertEquals(FallbackReason.ORDER_BY, diagnostics.snapshot().delivery().fallbackReason()); + verify(backend, never()).openReadSnapshot(); + verify(reads, never()).count(any()); + verify(reads, times(1)).find(any()); + } + } + + @Test + void aTraversalFailureBeforeAnyRowDoesNotClaimAnEmptyResult() { + StorageManager backend = mock(StorageManager.class, delegatesTo(storage)); + StorageManager snapshot = mock(StorageManager.class, delegatesTo(storage.openReadSnapshot())); + QueryOperations reads = mock(QueryOperations.class); + doReturn(snapshot).when(backend).openReadSnapshot(); + when(snapshot.queries()).thenReturn(reads); + when(reads.find(any())).thenThrow(new QueryEvaluationException("Read failed")); + var diagnostics = new ExecutionDiagnostics(); + try (var rows = new SparqlQueryExecutor(backend).withDiagnostics(diagnostics).evaluateTuple("SELECT * { ?s ?p ?o }")) { + assertThrows(QueryEvaluationException.class, rows::next); + assertEquals(FirstRowStatus.UNOBSERVED, diagnostics.snapshot().delivery().firstRowStatus()); + assertEquals(-1, diagnostics.snapshot().delivery().firstRowNanos()); + verify(snapshot, times(1)).close(); + } + } + + @Test + void guardClassificationDoesNotExecuteAnUnsupportedServiceOrExtension() { + assertEquals(FallbackReason.SERVICE, SelectCursor.fallbackReason(Query.create(Exp.create(Type.SERVICE)))); + var functional = Exp.create(Type.BIND); + functional.setFunctional(true); + assertEquals(FallbackReason.UNSUPPORTED_OPERATOR, SelectCursor.fallbackReason(Query.create(functional))); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/QueryExecutionDiagnosticsTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/QueryExecutionDiagnosticsTest.java new file mode 100644 index 000000000..e751ab492 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/engine/diagnostics/QueryExecutionDiagnosticsTest.java @@ -0,0 +1,436 @@ +package fr.inria.corese.core.next.query.impl.engine.diagnostics; + +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.data.api.term.IRI; +import fr.inria.corese.core.next.query.Repositories; +import fr.inria.corese.core.next.query.api.TupleQuery; +import fr.inria.corese.core.next.query.api.dataset.Dataset; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.exception.QueryTimeoutException; +import fr.inria.corese.core.next.query.api.repository.Repository; +import fr.inria.corese.core.next.storage.api.StorageManager; +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.operations.QueryOperations; +import fr.inria.corese.core.next.storage.impl.memory.MemoryStorageManager; +import fr.inria.corese.core.next.storage.impl.memory.StorageBenchmark; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class QueryExecutionDiagnosticsTest { + private static final String PREFIX = "PREFIX : "; + private static final String SELECTIVE = "SELECT ?s { ?s :type :Person . ?s :name \"name42\" }"; + @SuppressWarnings("java:S1450") // Test fixture shared between setup and cursor lifecycle tests + private QueryOperations realQueries; + private QueryOperations operations; + private Repository repository; + private final AtomicInteger opened = new AtomicInteger(); + private final AtomicInteger closed = new AtomicInteger(); + + @BeforeEach + void setUp() { + MemoryStorageManager storage = MemoryStorageManager.builder().build(); + storage.mutations().addAll(StorageBenchmark.data(200, 0, false)); + realQueries = storage.queries(); + operations = mock(QueryOperations.class, delegatesTo(realQueries)); + doAnswer(call -> { + opened.incrementAndGet(); + return realQueries.find(call.getArgument(0)).onClose(closed::incrementAndGet); + }).when(operations).find(any()); + StorageManager observed = mock(StorageManager.class, delegatesTo(storage)); + when(observed.queries()).thenReturn(operations); + doThrow(UnsupportedOperationException.class).when(observed).openReadSnapshot(); + when(observed.openSession()).thenAnswer(call -> { + StorageManager session = mock(StorageManager.class, delegatesTo(storage.openSession())); + when(session.queries()).thenReturn(operations); + doThrow(UnsupportedOperationException.class).when(session).openReadSnapshot(); + return session; + }); + repository = Repositories.create(observed); + } + + @AfterEach + void tearDown() { + repository.close(); + assertEquals(opened.get(), closed.get(), "Every storage cursor must be released"); + } + + @Test + void reportsChosenOrderEstimatesAndCandidatesWithoutExtraCounts() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery(PREFIX + SELECTIVE).evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals("urn:bench:s42", result.next().getValue("s").stringValue()); + assertFalse(result.hasNext()); + } + var profile = execution.profile(); + assertEquals(2, profile.patterns().size()); + var first = profile.patterns().getFirst(); + assertTrue(first.pattern().contains("urn:bench:name")); + assertEquals(1, first.estimatedCardinality()); + assertEquals(1, first.iteratorOpenings()); + assertEquals(1, first.candidateEdges()); + var second = profile.patterns().getLast(); + assertTrue(second.pattern().contains("urn:bench:type")); + assertEquals(200, second.estimatedCardinality()); + assertEquals(1, second.candidateEdges()); + assertFalse(first.auxiliary()); + assertNotEquals(first.location(), second.location()); + assertTrue(profile.compilationNanos() > 0); + assertTrue(profile.planningNanos() > 0); + assertTrue(profile.evaluationNanos() > 0); + assertTrue(profile.format().contains("estimate=200; iterations=1; candidates=1")); + assertThrows(UnsupportedOperationException.class, () -> profile.patterns().clear()); + verify(operations, times(2)).count(any()); + verify(operations, times(2)).find(any()); + assertEquals(opened.get(), closed.get(), "Evaluation releases snapshots before returning"); + } + } + + @ParameterizedTest + @ValueSource(ints = {1, 10}) + void limitsCountOnlyConsumedCandidatesAndDoNotRequestSingletonEstimates(int limit) { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery("SELECT * { ?s ?p ?o } LIMIT " + limit) + .evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals(limit, result.stream().count()); + } + var pattern = execution.profile().patterns().getFirst(); + assertEquals(-1, pattern.estimatedCardinality()); + assertEquals(1, pattern.iteratorOpenings()); + assertEquals(limit, pattern.candidateEdges()); + verify(operations, never()).count(any()); + } + } + + @Test + void askStopsAtItsFirstCandidateAndReleasesTheSnapshot() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareBooleanQuery("ASK { ?s ?p ?o }").evaluateWithDiagnostics(); + assertTrue(execution.result()); + assertEquals(1, execution.profile().patterns().getFirst().candidateEdges()); + assertEquals(opened.get(), closed.get()); + verify(operations, never()).count(any()); + } + } + + @Test + void repeatedJoinInvocationsAreSummedRatherThanReportedAsDistinctRows() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery(PREFIX + + "SELECT ?s { ?s :type :Person . ?s :name ?name }").evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals(200, result.stream().count()); + } + var patterns = execution.profile().patterns(); + assertEquals(200, patterns.getFirst().candidateEdges()); + assertEquals(1, patterns.getFirst().iteratorOpenings()); + assertEquals(200, patterns.getLast().candidateEdges()); + assertEquals(200, patterns.getLast().iteratorOpenings()); + } + } + + @Test + void unvisitedPatternsRetainTheirPlanPositionAndZeroCounters() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery(PREFIX + + "SELECT * { ?s :type :Missing . ?s :name ?name }").evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertFalse(result.hasNext()); + } + assertEquals(0, execution.profile().patterns().getFirst().estimatedCardinality()); + assertEquals(0, execution.profile().patterns().getLast().iteratorOpenings()); + assertEquals(0, execution.profile().patterns().getLast().candidateEdges()); + } + } + + @Test + void candidateCountsPrecedeRdfTermMatching() { + try (var connection = repository.getConnection()) { + connection.add(iri("a"), iri("self"), iri("a")); + connection.add(iri("a"), iri("self"), iri("b")); + var execution = connection.prepareTupleQuery(PREFIX + "SELECT ?s { ?s :self ?s }") + .evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals(1, result.stream().count()); + } + assertEquals(2, execution.profile().patterns().getFirst().candidateEdges()); + } + } + + @Test + void orderByLimitStillReportsTheFullScanRequiredForSorting() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery(PREFIX + + "SELECT ?name { ?s :name ?name } ORDER BY ?name LIMIT 10").evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals(10, result.stream().count()); + } + assertEquals(200, execution.profile().patterns().getFirst().candidateEdges()); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT ?s { ?s :name \"name42\" OPTIONAL { ?s :missing ?o } }", + "SELECT ?s { ?s :name \"name42\" MINUS { ?s :name \"name41\" } }", + "SELECT ?s ?v { ?s :name \"name42\" BIND(?s AS ?v) FILTER(?s = :s42) }", + "SELECT (COUNT(?s) AS ?n) { ?s :type :Person }" + }) + void instrumentationPreservesSolutionsAcrossSemanticOperators(String sparql) { + try (var connection = repository.getConnection()) { + TupleQuery query = connection.prepareTupleQuery(PREFIX + sparql); + java.util.List ordinary; + try (var result = query.evaluate()) { + ordinary = result.stream().map(row -> row.getBindingNames().stream().sorted() + .map(name -> name + "=" + row.getValue(name)).toList().toString()).sorted().toList(); + } + var execution = query.evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals(ordinary, result.stream().map(row -> row.getBindingNames().stream().sorted() + .map(name -> name + "=" + row.getValue(name)).toList().toString()).sorted().toList()); + } + } + } + + @Test + void unknownCountsDoNotPreventExecution() { + doThrow(new UnsupportedOperationException("No estimates")).when(operations).count(any()); + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery(PREFIX + SELECTIVE).evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals(1, result.stream().count()); + } + assertTrue(execution.profile().patterns().stream().allMatch(p -> p.estimatedCardinality() == -1)); + assertTrue(execution.profile().format().contains("estimate=unknown")); + } + } + + @Test + void repeatedEvaluationsUseFreshCountersAndCurrentDataDatasetAndBindings() { + try (var connection = repository.getConnection()) { + TupleQuery query = connection.prepareTupleQuery(PREFIX + SELECTIVE); + var before = query.evaluateWithDiagnostics(); + try (var result = before.result()) { + assertEquals(1, result.stream().count()); + } + connection.add(iri("other"), iri("name"), Values.factory().createLiteral("name42"), iri("g")); + connection.add(iri("other"), iri("type"), iri("Person"), iri("g")); + query.setDataset(Dataset.builder().defaultGraph(iri("g")).build()); + query.setBinding("s", iri("other")); + var after = query.evaluateWithDiagnostics(); + try (var result = after.result()) { + assertEquals("urn:bench:other", result.next().getValue("s").stringValue()); + assertFalse(result.hasNext()); + } + assertEquals(200, before.profile().patterns().getLast().estimatedCardinality()); + assertTrue(after.profile().patterns().stream().allMatch(p -> p.estimatedCardinality() == 1)); + assertTrue(after.profile().patterns().stream().allMatch(p -> p.candidateEdges() == 1)); + // An ordinary evaluation after profiling must not update a prior report. + try (var result = query.evaluate()) { + assertEquals(1, result.stream().count()); + } + assertEquals(1, after.profile().patterns().getFirst().candidateEdges()); + } + } + + @Test + void unionBranchesKeepSeparatePatternIdentitiesAndSolutionBags() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery(PREFIX + + "SELECT ?s { { ?s :name \"name42\" } UNION { ?s :name \"name42\" } }") + .evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals(2, result.stream().count()); + } + var patterns = execution.profile().patterns(); + assertEquals(2, patterns.size()); + assertTrue(patterns.stream().allMatch(p -> !p.auxiliary() && p.candidateEdges() == 1)); + assertNotEquals(patterns.getFirst().location(), patterns.getLast().location()); + } + } + + @Test + void namedGraphScopeIsVisibleAndPreserved() { + try (var connection = repository.getConnection()) { + connection.add(iri("s"), iri("p"), iri("o"), iri("g")); + var execution = connection.prepareTupleQuery(PREFIX + + "SELECT * { GRAPH :g { ?s :p ?o } }").evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals(1, result.stream().count()); + } + assertTrue(execution.profile().patterns().getFirst().pattern().contains("GRAPH")); + assertTrue(execution.profile().patterns().getFirst().pattern().contains("urn:bench:g")); + } + } + + @Test + void producerCountersCountAfterInterGraphDeduplication() { + try (var connection = repository.getConnection()) { + for (String graph : new String[]{"g1", "g2"}) { + connection.add(iri("s42"), iri("name"), Values.factory().createLiteral("name42"), iri(graph)); + connection.add(iri("s42"), iri("type"), iri("Person"), iri(graph)); + } + var execution = connection.prepareTupleQuery(PREFIX + SELECTIVE) + .setDataset(Dataset.builder().defaultGraph(iri("g1")).defaultGraph(iri("g2")).build()) + .evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals(1, result.stream().count()); + } + assertEquals(2, execution.profile().patterns().getFirst().estimatedCardinality()); + assertEquals(1, execution.profile().patterns().getFirst().candidateEdges()); + } + } + + @Test + void subqueryPatternsKeepTheirScope() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery(PREFIX + + "SELECT ?s { { SELECT ?s { ?s :type :Person . ?s :name \"name42\" } } ?s :group ?g }") + .evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals(1, result.stream().count()); + } + assertEquals(3, execution.profile().patterns().size()); + assertTrue(execution.profile().patterns().stream().noneMatch(QueryExecutionProfile.Pattern::auxiliary)); + assertTrue(execution.profile().patterns().stream().allMatch(p -> p.candidateEdges() == 1)); + } + } + + @Test + void pathsIncludeLogicalCandidatesAndExplicitAuxiliaryLookups() { + try (var connection = repository.getConnection()) { + connection.add(iri("a"), iri("link"), iri("b")); + connection.add(iri("b"), iri("link"), iri("c")); + var execution = connection.prepareTupleQuery(PREFIX + + "SELECT ?o { :a :link/:link ?o }").evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals("urn:bench:c", result.next().getValue("o").stringValue()); + assertFalse(result.hasNext()); + } + var patterns = execution.profile().patterns(); + assertFalse(patterns.getFirst().auxiliary()); + assertEquals(1, patterns.getFirst().candidateEdges()); + assertTrue(patterns.stream().anyMatch(QueryExecutionProfile.Pattern::auxiliary)); + } + } + + @Test + void existsReportsOnlyActuallyExecutedAuxiliaryPatterns() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery(PREFIX + + "SELECT ?s { ?s :name \"name42\" FILTER EXISTS { ?s :type :Person } }") + .evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals(1, result.stream().count()); + } + var patterns = execution.profile().patterns(); + assertEquals(2, patterns.size(), "Do not reconstruct unexecuted EXISTS trees for display"); + assertTrue(patterns.getLast().auxiliary()); + assertEquals(1, patterns.getLast().candidateEdges()); + } + } + + @Test + void auxiliaryDescriptionsRetainLiteralDatatypesAndDoNotMergeDifferentTerms() { + try (var connection = repository.getConnection()) { + connection.add(iri("s42"), iri("mark"), Values.factory().createLiteral("1")); + connection.add(iri("s42"), iri("mark"), Values.factory().createLiteral("1", Values.factory().createIRI( + "http://www.w3.org/2001/XMLSchema#integer"))); + var execution = connection.prepareTupleQuery(PREFIX + """ + SELECT ?s { + { ?s :name "name42" FILTER EXISTS { ?s :mark "1" } } + UNION + { ?s :name "name42" FILTER EXISTS { ?s :mark 1 } } + } + """).evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertEquals(2, result.stream().count()); + } + var auxiliary = execution.profile().patterns().stream() + .filter(QueryExecutionProfile.Pattern::auxiliary).toList(); + assertEquals(2, auxiliary.size()); + assertNotEquals(auxiliary.getFirst().pattern(), auxiliary.getLast().pattern()); + assertTrue(auxiliary.stream().anyMatch(p -> p.pattern().contains("^^<"))); + } + } + + @ParameterizedTest + @ValueSource(strings = {"CONSTRUCT { ?s ?o } WHERE { ?s ?p ?o } LIMIT 1", "DESCRIBE "}) + void graphQueriesReturnTheUsualOwnedResultWithAProfile(String sparql) { + try (var connection = repository.getConnection()) { + var execution = connection.prepareGraphQuery(sparql).evaluateWithDiagnostics(); + try (var result = execution.result()) { + assertTrue(result.hasNext()); + } + assertFalse(execution.profile().patterns().isEmpty()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void storageFailuresKeepTheirCauseAndReleaseCursors(boolean duringTraversal) { + StorageException cause = new StorageException(ErrorCode.QUERY_FAILED, "Backend failure"); + if (duringTraversal) { + doAnswer(call -> { + opened.incrementAndGet(); + return realQueries.find(call.getArgument(0)) + .peek(statement -> { throw cause; }).onClose(closed::incrementAndGet); + }).when(operations).find(any()); + } else { + doThrow(cause).when(operations).find(any()); + } + try (var connection = repository.getConnection()) { + var query = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }"); + var failure = assertThrows(QueryEvaluationException.class, query::evaluateWithDiagnostics); + assertSame(cause, failure.getCause()); + assertEquals(opened.get(), closed.get()); + } + } + + @Test + void timeoutReleasesItsCursorsAndDoesNotContaminateTheFollowingQuery() { + doAnswer(call -> { + opened.incrementAndGet(); + return realQueries.find(call.getArgument(0)).peek(statement -> { + try { + Thread.sleep(1000); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new QueryEvaluationException("Interrupted", interrupted); + } + }).onClose(closed::incrementAndGet); + }).when(operations).find(any()); + try (var connection = repository.getConnection()) { + var query = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }"); + query.setTimeout(Duration.ofMillis(30)); + assertThrows(QueryTimeoutException.class, query::evaluateWithDiagnostics); + assertFalse(Thread.currentThread().isInterrupted()); + assertEquals(opened.get(), closed.get()); + } + doAnswer(call -> { + opened.incrementAndGet(); + return realQueries.find(call.getArgument(0)).onClose(closed::incrementAndGet); + }).when(operations).find(any()); + try (var connection = repository.getConnection()) { + assertTrue(connection.prepareBooleanQuery("ASK { ?s ?p ?o }").evaluateWithDiagnostics().result()); + } + } + + private static IRI iri(String local) { + return Values.factory().createIRI("urn:bench:" + local); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalJoinTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalJoinTest.java new file mode 100644 index 000000000..0c6499ebf --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/engine/eval/EvalJoinTest.java @@ -0,0 +1,98 @@ +package fr.inria.corese.core.next.query.impl.engine.eval; + +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; +import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; +import fr.inria.corese.core.next.query.impl.engine.solution.Memory; +import fr.inria.corese.core.next.query.impl.engine.spi.Producer; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +class EvalJoinTest { + private static final int LEVEL = 3; + private final Eval engine = mock(Eval.class); + private final Memory environment = mock(Memory.class); + private final Producer producer = mock(Producer.class); + private final Stack stack = mock(Stack.class); + private final Node variable = mock(Node.class); + private final Node value = mock(Node.class); + private final Mapping leftRow = mock(Mapping.class); + private final Mapping rightRow = mock(Mapping.class); + private final EvalJoin join = new EvalJoin(engine); + + EvalJoinTest() { + when(engine.getMemory()).thenReturn(environment); + when(environment.push(any(Mapping.class), eq(LEVEL))).thenReturn(true); + when(leftRow.getNode(variable)).thenReturn(value); + when(rightRow.getNode(variable)).thenReturn(value); + when(value.match(value)).thenReturn(true); + } + + @ParameterizedTest + @CsvSource({"true, -2", "true, 2", "true, 3", "false, -2", "false, 2", "false, 3"}) + void preservesBacktrackingAndBagMultiplicity(boolean sharedVariable, int continuation) { + Mappings left = rows(List.of(leftRow, leftRow)); + Mappings right = rows(List.of(rightRow, rightRow, rightRow)); + when(right.find(value, variable)).thenReturn(0); + when(engine.eval(producer, null, stack, LEVEL + 1)).thenReturn(continuation); + + int actual = sharedVariable + ? join.joinWithCommonVariable(variable, producer, null, stack, environment, left, right, LEVEL) + : join.joinWithoutCommonVariable(producer, null, stack, environment, left, right, LEVEL); + + assertEquals(continuation, actual); + int pairs = continuation < LEVEL ? 1 : 6; + verify(engine, times(pairs)).eval(producer, null, stack, LEVEL + 1); + verify(environment, times(pairs)).pop(rightRow); + verify(environment, times(continuation < LEVEL ? 0 : 2)).pop(leftRow); + } + + @Test + void unboundPrefixBackjumpDoesNotContinueIntoBoundCandidates() { + Mapping unbound = mock(Mapping.class); + Mappings right = rows(List.of(unbound, rightRow)); + when(engine.eval(producer, null, stack, LEVEL + 1)).thenReturn(LEVEL - 1); + + int actual = join.joinWithCommonVariable(variable, producer, null, stack, environment, + rows(List.of(leftRow)), right, LEVEL); + + assertEquals(LEVEL - 1, actual); + verify(environment).pop(unbound); + verify(environment, never()).push(rightRow, LEVEL); + verify(right, never()).find(value, variable); + } + + @Test + void absentCandidateDoesNotBackjumpBeforeTheNextLeftRow() { + Mapping otherLeft = mock(Mapping.class); + when(otherLeft.getNode(variable)).thenReturn(value); + Mappings right = rows(List.of(rightRow, rightRow)); + when(right.find(value, variable)).thenReturn(-1, 0); + when(engine.eval(producer, null, stack, LEVEL + 1)).thenReturn(LEVEL); + + int actual = join.joinWithCommonVariable(variable, producer, null, stack, environment, + rows(List.of(leftRow, otherLeft)), right, LEVEL); + + assertEquals(LEVEL, actual); + verify(environment).pop(leftRow); + verify(environment).pop(otherLeft); + verify(engine, times(2)).eval(producer, null, stack, LEVEL + 1); + } + + private static Mappings rows(List rows) { + Mappings mappings = mock(Mappings.class); + when(mappings.size()).thenReturn(rows.size()); + when(mappings.iterator()).thenAnswer(invocation -> rows.iterator()); + for (int index = 0; index < rows.size(); index++) { + when(mappings.get(index)).thenReturn(rows.get(index)); + } + return mappings; + } +} 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 f7fb6ab5b..7f8a3c1e3 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 @@ -6,10 +6,8 @@ import fr.inria.corese.core.next.query.impl.engine.spi.Environment; import fr.inria.corese.core.next.query.impl.engine.spi.Evaluator; import fr.inria.corese.core.next.query.impl.engine.spi.Matcher; -import fr.inria.corese.core.next.query.impl.engine.spi.Plugin; import fr.inria.corese.core.next.query.impl.engine.spi.Producer; import fr.inria.corese.core.next.query.impl.engine.spi.Provider; -import fr.inria.corese.core.next.query.impl.engine.spi.SPARQLEngine; import fr.inria.corese.core.next.query.impl.engine.model.BindingContext; import fr.inria.corese.core.next.query.impl.engine.event.KgramEventDispatcher; @@ -67,12 +65,6 @@ void testCreateEvalFactory() { assertNotNull(e, "Factory-created eval should not be null"); } - @Test - @DisplayName("Should create empty eval") - void testCreateEmptyEval() { - Eval e = new Eval(); - assertNotNull(e, "Empty eval should not be null"); - } } @Nested @@ -269,21 +261,6 @@ void testGetListenerDefault() { } } - @Nested - @DisplayName("SPARQL Engine Tests") - class SPARQLEngineTests { - - @Test - @DisplayName("Should set and get SPARQL engine") - void testSetAndGetSPARQLEngine() { - SPARQLEngine mockEngine = mock(SPARQLEngine.class); - eval.setSPARQLEngine(mockEngine); - assertEquals(mockEngine, eval.getSPARQLEngine(), - "SPARQL engine should match"); - } - } - - @Nested @DisplayName("Stop Flag Tests") class StopFlagTests { @@ -347,19 +324,6 @@ void testCopyWithExtern() { } } - @Nested - @DisplayName("Plugin Tests") - class PluginTests { - - @Test - @DisplayName("Should add plugin") - void testAddPlugin() { - Plugin mockPlugin = mock(Plugin.class); - assertDoesNotThrow(() -> eval.add(mockPlugin), - "Adding plugin should not throw"); - } - } - @Nested @DisplayName("Evaluation Configuration Tests") class EvaluationConfigurationTests { @@ -458,15 +422,4 @@ void testIsJoinMappings() { } } - @Nested - @DisplayName("Clean Tests") - class CleanTests { - - @Test - @DisplayName("Should clean eval state") - void testClean() { - assertDoesNotThrow(() -> eval.clean(), - "Clean should not throw exception"); - } - } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/engine/path/GraphPathTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/engine/path/GraphPathTest.java deleted file mode 100644 index 6df745042..000000000 --- a/src/test/java/fr/inria/corese/core/next/query/impl/engine/path/GraphPathTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.path; - -import fr.inria.corese.core.next.query.impl.engine.model.Node; -import fr.inria.corese.core.next.query.impl.engine.spi.Environment; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.mockito.Mockito.*; - -class GraphPathTest { - @Test - void runsPathEnumerationOnTheWorkerThread() throws InterruptedException { - PathFinder finder = mock(PathFinder.class); - Environment environment = mock(Environment.class); - Node start = mock(Node.class); - when(finder.getIndex()).thenReturn(1); - when(finder.get(environment, 1)).thenReturn(start); - GraphPath worker = new GraphPath(finder, environment); - - worker.start(); - worker.join(2000); - - assertFalse(worker.isAlive()); - verify(finder).process(start, environment); - } -} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/engine/path/PathMappingBufferTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/engine/path/PathMappingBufferTest.java deleted file mode 100644 index 2e50be92f..000000000 --- a/src/test/java/fr/inria/corese/core/next/query/impl/engine/path/PathMappingBufferTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package fr.inria.corese.core.next.query.impl.engine.path; - -import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -import java.util.NoSuchElementException; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -import static org.junit.jupiter.api.Assertions.*; - -@Timeout(5) -class PathMappingBufferTest { - @Test - void consumesProducedMappingsAndKeepsTheEndOfStream() throws Exception { - PathMappingBuffer buffer = new PathMappingBuffer(); - Mapping mapping = Mapping.create(); - try (var executor = Executors.newSingleThreadExecutor()) { - var producer = executor.submit(() -> { - buffer.put(mapping, true); - buffer.put(null, false); - }); - assertTrue(buffer.hasNext()); - assertSame(mapping, buffer.next()); - assertFalse(buffer.hasNext()); - assertThrows(NoSuchElementException.class, buffer::next); - assertFalse(buffer.hasNext()); - producer.get(2, TimeUnit.SECONDS); - } - } - - @Test - void interruptionEndsIterationAndPreservesTheInterruptFlag() { - PathMappingBuffer buffer = new PathMappingBuffer(); - Thread.currentThread().interrupt(); - try { - assertThrows(NoSuchElementException.class, buffer::next); - assertTrue(Thread.currentThread().isInterrupted()); - } finally { - Thread.interrupted(); - } - } -} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/engine/pattern/CompiledQueryPlanTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/engine/pattern/CompiledQueryPlanTest.java new file mode 100644 index 000000000..3e211d0ac --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/engine/pattern/CompiledQueryPlanTest.java @@ -0,0 +1,96 @@ +package fr.inria.corese.core.next.query.impl.engine.pattern; + +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.model.NodeImpl; +import fr.inria.corese.core.next.query.impl.engine.storage.ProducerDefault; +import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst; +import fr.inria.corese.core.next.query.impl.sparql.bridge.CoreseAstQueryBuilder; +import fr.inria.corese.core.next.query.impl.sparql.parser.SparqlParser; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class CompiledQueryPlanTest { + private static final String TEXT = """ + SELECT ?s ?label { + VALUES ?s { } + ?s ?o + BIND(STR(?o) AS ?label) + FILTER EXISTS { ?s ?o } + } + """; + + @Test + void captureDetachesTheCompilerAndEveryExecutionHasIndependentMutableState() { + Query compiler = query(); + CompiledQueryPlan image = CompiledQueryPlan.capture(compiler, Map.of()); + Query first = image.newExecution(null); + Query second = image.newExecution(null); + assertNotSame(first, second); + assertEquals(first.getSelect().size(), second.getSelect().size()); + Node firstVariable = first.getSelect().getFirst(); + Node secondVariable = second.getSelect().getFirst(); + assertNotSame(firstVariable, secondVariable); + int compiledIndex = secondVariable.getIndex(); + firstVariable.setIndex(999); + first.getSelect().clear(); + first.setLimit(1); + compiler.getSelect().clear(); + compiler.setLimit(2); + assertEquals(compiledIndex, secondVariable.getIndex()); + Query fresh = image.newExecution(null); + assertEquals(2, fresh.getSelect().size()); + assertEquals(Integer.MAX_VALUE, fresh.getLimit()); + assertEquals(compiledIndex, fresh.getSelect().getFirst().getIndex()); + assertEquals(2, second.getSelect().size()); + } + + @Test + void valuesRowsAndFilterIndexesAreNotShared() { + CompiledQueryPlan image = CompiledQueryPlan.capture(query(), Map.of()); + Query first = image.newExecution(null); + Query second = image.newExecution(null); + Exp firstValues = operators(first).stream().filter(e -> e.type() == Exp.Type.VALUES).findFirst().orElseThrow(); + Exp secondValues = operators(second).stream().filter(e -> e.type() == Exp.Type.VALUES).findFirst().orElseThrow(); + assertNotSame(firstValues.getMappings(), secondValues.getMappings()); + assertNotSame(firstValues.getMappings().get(0), secondValues.getMappings().get(0)); + firstValues.getMappings().get(0).getTargetNodes()[0] = NodeImpl.forIRI("urn:changed"); + assertEquals("urn:s", secondValues.getMappings().get(0).getTargetNodes()[0].getLabel()); + Exp firstBind = operators(first).stream().filter(Exp::isBind).findFirst().orElseThrow(); + Exp secondBind = operators(second).stream().filter(Exp::isBind).findFirst().orElseThrow(); + assertNotSame(firstBind.getFilter(), secondBind.getFilter()); + int index = secondBind.getFilter().getExp().getIndex(); + firstBind.getFilter().getExp().setIndex(731); + assertEquals(index, secondBind.getFilter().getExp().getIndex()); + firstValues.getMappings().clear(); + assertEquals(2, secondValues.getMappings().size()); + } + + @Test + void copiesPreserveIdentityWithinAFrameAndRejectUnknownMutablePayloads() { + Query first = CompiledQueryPlan.capture(query(), Map.of()).newExecution(null); + Exp values = operators(first).stream().filter(e -> e.type() == Exp.Type.VALUES).findFirst().orElseThrow(); + Node variable = first.getSelect().getFirst(); + assertSame(variable, values.getNodeList().getFirst()); + assertSame(variable, values.getMappings().get(0).getQueryNodes()[0]); + assertThrows(IllegalArgumentException.class, () -> new PlanCopy().copy(new StringBuilder())); + } + + private static Query query() { + var ast = (SelectQueryAst) new SparqlParser().parse(TEXT); + Query query = new CoreseAstQueryBuilder().toNextQuery(ast); + query.complete(new ProducerDefault()); + return query; + } + + private static List operators(Exp expression) { + List result = new ArrayList<>(); + result.add(expression); + for (Exp child : expression) result.addAll(operators(child)); + return result; + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpTest.java index 1c482ae3b..6e3f149e8 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/engine/pattern/ExpTest.java @@ -5,7 +5,6 @@ import fr.inria.corese.core.next.query.impl.engine.model.ExpType; import fr.inria.corese.core.next.query.impl.engine.model.Filter; import fr.inria.corese.core.next.query.impl.engine.model.Node; -import fr.inria.corese.core.next.query.impl.engine.model.Regex; import fr.inria.corese.core.next.query.impl.engine.solution.Mappings; import fr.inria.corese.core.next.query.impl.engine.spi.Producer; @@ -442,46 +441,9 @@ void testSetAndIsGenerated() { assertTrue(exp.isGenerated(), "Generated should be true"); } - @Test - @DisplayName("Should set and check BGPAble flag") - void testSetAndIsBGPAble() { - exp.setBGPAble(true); - assertTrue(exp.isBGPAble(), "BGPAble should be true"); - } } - @Nested - @DisplayName("Min/Max Tests") - class MinMaxTests { - - @Test - @DisplayName("Should set and get min") - void testSetAndGetMin() { - exp.setMin(5); - assertEquals(5, exp.getMin(), "Min should be 5"); - } - - @Test - @DisplayName("Should set and get max") - void testSetAndGetMax() { - exp.setMax(10); - assertEquals(10, exp.getMax(), "Max should be 10"); - } - @Test - @DisplayName("Should have default min -1") - void testDefaultMin() { - Exp e = Exp.create(ExpType.Type.AND); - assertEquals(-1, e.getMin(), "Default min should be -1"); - } - - @Test - @DisplayName("Should have default max -1") - void testDefaultMax() { - Exp e = Exp.create(ExpType.Type.AND); - assertEquals(-1, e.getMax(), "Default max should be -1"); - } - } @Nested @DisplayName("Level Tests") @@ -547,18 +509,6 @@ void testSetAndGetProducer() { } } - @Nested - @DisplayName("Regex Tests") - class RegexTests { - - @Test - @DisplayName("Should set and get regex") - void testSetAndGetRegex() { - Regex mockRegex = mock(Regex.class); - exp.setRegex(mockRegex); - assertEquals(mockRegex, exp.getRegex(), "Regex should match"); - } - } @Nested @DisplayName("Mappings Tests") @@ -595,18 +545,6 @@ void testSetAndGetStack() { } } - @Nested - @DisplayName("Path Expression Tests") - class PathExpressionTests { - - @Test - @DisplayName("Should set and get path expression") - void testSetAndGetPath() { - Exp pathExp = Exp.create(ExpType.Type.PATH); - exp.setPath(pathExp); - assertEquals(pathExp, exp.getPath(), "Path expression should match"); - } - } @Nested @DisplayName("Bind Expression Tests") @@ -763,18 +701,6 @@ void testGetStatement() { } } - @Nested - @DisplayName("Cache Tests") - class CacheTests { - - @Test - @DisplayName("Should set and get cache node") - void testSetAndGetCacheNode() { - exp.setCacheNode(mockNode); - assertEquals(mockNode, exp.getCacheNode(), "Cache node should match"); - } - } - @Nested @DisplayName("InScope Node List Tests") class InScopeNodeListTests { diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/engine/pattern/QueryTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/engine/pattern/QueryTest.java index 80f53229e..41b02d64f 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/engine/pattern/QueryTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/engine/pattern/QueryTest.java @@ -56,9 +56,9 @@ void testCreateQueryFactory() { } @Test - @DisplayName("Should create query with type") + @DisplayName("Should create an empty query") void testCreateQueryWithType() { - Query q = Query.create(0); + Query q = Query.create(); assertNotNull(q, "Query should not be null"); } } @@ -510,13 +510,6 @@ void testSetAndGetGraphNode() { query.setGraphNode(mockNode); assertEquals(mockNode, query.getGraphNode(), "Graph node should match"); } - - @Test - @DisplayName("Should get path node") - void testGetPathNode() { - assertDoesNotThrow(() -> query.getPathNode(), - "Getting path node should not throw"); - } } @Nested @@ -593,19 +586,6 @@ void testIsDetail() { assertTrue(result, "Should be detail by default"); } - @Test - @DisplayName("Should set and check synchronized flag") - void testSetAndIsSynchronized() { - query.setSynchronized(true); - assertTrue(query.isSynchronized(), "Query should be synchronized"); - } - - @Test - @DisplayName("Should set and check algebra flag") - void testSetAndIsAlgebra() { - query.setAlgebra(true); - assertTrue(query.isAlgebra(), "Query should be algebra"); - } } @Nested @@ -628,25 +608,6 @@ void testSetAndGetService() { } } - @Nested - @DisplayName("Template Tests") - class TemplateTests { - - @Test - @DisplayName("Should set and check if is template") - void testSetAndIsTemplate() { - query.setTemplate(true); - assertTrue(query.isTemplate(), "Query should be template"); - } - - @Test - @DisplayName("Should check if is transformation template") - void testIsTransformationTemplate() { - boolean result = query.isTransformationTemplate(); - assertFalse(result, "Should not be transformation template by default"); - } - } - @Nested @DisplayName("Global Query Tests") class GlobalQueryTests { @@ -674,38 +635,6 @@ void testGetOuterQuery() { } } - @Nested - @DisplayName("Extension Tests") - class ExtensionTests { - - @Test - @DisplayName("Should set and check if is extension") - void testSetAndIsExtension() { - query.setExtension(true); - assertTrue(query.isExtension(), "Query should be extension"); - } - - } - - @Nested - @DisplayName("Parallel and Lock Tests") - class ParallelLockTests { - - @Test - @DisplayName("Should set and check parallel flag") - void testSetAndIsParallel() { - query.setParallel(false); - assertFalse(query.isParallel(), "Query should not be parallel"); - } - - @Test - @DisplayName("Should set and check lock flag") - void testSetAndIsLock() { - query.setLock(false); - assertFalse(query.isLock(), "Query should not be lock"); - } - } - @Nested @DisplayName("Federate and Validate Tests") class FederateValidateTests { diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/engine/planning/CardinalityQueryPlannerTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/engine/planning/CardinalityQueryPlannerTest.java new file mode 100644 index 000000000..1ff1d97b8 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/engine/planning/CardinalityQueryPlannerTest.java @@ -0,0 +1,343 @@ +package fr.inria.corese.core.next.query.impl.engine.planning; + +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.data.api.term.IRI; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.impl.engine.model.Edge; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.model.NodeImpl; +import fr.inria.corese.core.next.query.impl.engine.path.PropertyPathEdge; +import fr.inria.corese.core.next.query.impl.engine.pattern.Exp; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; +import fr.inria.corese.core.next.query.impl.engine.storage.ProducerDefault; +import fr.inria.corese.core.next.query.impl.engine.storage.StorageManagerProducer; +import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst; +import fr.inria.corese.core.next.query.impl.sparql.bridge.CoreseAstQueryBuilder; +import fr.inria.corese.core.next.query.impl.sparql.execution.SparqlQueryExecutor; +import fr.inria.corese.core.next.query.impl.sparql.parser.SparqlParser; +import fr.inria.corese.core.next.storage.api.StorageManager; +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.model.StatementPattern; +import fr.inria.corese.core.next.storage.api.operations.QueryOperations; +import fr.inria.corese.core.next.storage.impl.memory.MemoryStorageManager; +import fr.inria.corese.core.next.storage.impl.memory.StorageBenchmark; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class CardinalityQueryPlannerTest { + + private static final String PREFIX = "PREFIX : "; + private static final String BROAD_THEN_SELECTIVE = "?s :type :Person . ?s :name \"name42\""; + private MemoryStorageManager storage; + + @BeforeEach + void setUp() { + storage = MemoryStorageManager.builder().build(); + storage.mutations().addAll(StorageBenchmark.data(200, 0, false)); + } + + @AfterEach + void close() { + storage.close(); + } + + @Test + void choosesSelectiveFirstPatternWithoutOpeningCursorsOrTransactions() { + Query query = query(BROAD_THEN_SELECTIVE); + QueryOperations operations = mock(QueryOperations.class, delegatesTo(storage.queries())); + StorageManager observed = mock(StorageManager.class); + when(observed.queries()).thenReturn(operations); + doThrow(UnsupportedOperationException.class).when(observed).openReadSnapshot(); + + query.complete(new StorageManagerProducer(observed)); + + assertEquals(List.of("urn:bench:name", "urn:bench:type"), predicates(query)); + verify(operations, times(2)).count(any()); + verify(operations, never()).find(any()); + verify(observed, never()).transactions(); + verify(observed, never()).openSession(); + } + + @Test + void executionStartsWithSelectivePatternAndReadsOnlyTwoStatements() { + List scans = new ArrayList<>(); + int[] visited = {0}; + QueryOperations operations = mock(QueryOperations.class, delegatesTo(storage.queries())); + doAnswer(call -> { + StatementPattern pattern = call.getArgument(0); + scans.add(pattern); + return storage.queries().find(pattern).peek(statement -> visited[0]++); + }).when(operations).find(any()); + StorageManager observed = mock(StorageManager.class, delegatesTo(storage)); + when(observed.queries()).thenReturn(operations); + doThrow(UnsupportedOperationException.class).when(observed).openReadSnapshot(); + + try (var rows = new SparqlQueryExecutor(observed).evaluateTuple( + PREFIX + "SELECT ?s { " + BROAD_THEN_SELECTIVE + " }")) { + assertEquals("urn:bench:s42", rows.next().getValue("s").stringValue()); + assertFalse(rows.hasNext()); + } + assertEquals(List.of(iri("name"), iri("type")), scans.stream() + .map(StatementPattern::getPredicate).toList()); + assertEquals(2, visited[0]); + } + + @Test + void connectedJoinsPrecedeDisconnectedPatternsAfterSelectiveSeed() { + Query query = query("?s :link ?v . ?u :unrelated ?z . ?s :seed ?x"); + query.complete(new StatisticsProducer(Map.of("urn:bench:link", 100L, + "urn:bench:unrelated", 2L, "urn:bench:seed", 1L))); + assertEquals(List.of("urn:bench:seed", "urn:bench:link", "urn:bench:unrelated"), predicates(query)); + } + + @Test + void variablePredicatesParticipateInJoinConnectivity() { + Query query = query("?s ?p :seed . ?u :unrelated ?z . ?p :link ?x"); + query.complete(new StatisticsProducer(Map.of("urn:bench:link", 100L, "urn:bench:unrelated", 2L)) { + @Override + public long estimateCardinality(Query scope, Node graph, Edge edge) { + return edge.getEdgeVariable() == null ? super.estimateCardinality(scope, graph, edge) : 1; + } + }); + List plan = edges(query.getBody()); + assertEquals("p", plan.getFirst().getEdgeVariable().getLabel()); + assertEquals("urn:bench:link", plan.get(1).getEdgeLabel()); + assertEquals("urn:bench:unrelated", plan.get(2).getEdgeLabel()); + } + + @Test + void cardinalitiesRetainLongPrecision() { + Query query = query("?s :large ?x . ?s :small ?y"); + query.complete(new StatisticsProducer(Map.of("urn:bench:large", Long.MAX_VALUE, + "urn:bench:small", 3_000_000_000L))); + assertEquals(List.of("urn:bench:small", "urn:bench:large"), predicates(query)); + } + + @ParameterizedTest + @ValueSource(longs = {0, QueryStatistics.UNKNOWN_CARDINALITY, 7}) + void equalOrUnavailableStatisticsKeepStableSourceOrder(long count) { + Query query = query("?s :left ?x . ?s :right ?y"); + query.complete(new StatisticsProducer(Map.of("urn:bench:left", count, "urn:bench:right", count))); + assertEquals(List.of("urn:bench:left", "urn:bench:right"), predicates(query)); + } + + @Test + void producerWithoutStatisticsRetainsConnectedFallback() { + Query query = query(BROAD_THEN_SELECTIVE); + query.complete(new ProducerDefault()); + assertEquals(List.of("urn:bench:type", "urn:bench:name"), predicates(query)); + } + + @Test + void backendWithoutCountSupportCanStillExecuteQueries() { + QueryOperations operations = mock(QueryOperations.class, delegatesTo(storage.queries())); + doThrow(new UnsupportedOperationException("Counts unavailable")).when(operations).count(any()); + StorageManager observed = mock(StorageManager.class, delegatesTo(storage)); + when(observed.queries()).thenReturn(operations); + doThrow(UnsupportedOperationException.class).when(observed).openReadSnapshot(); + try (var rows = new SparqlQueryExecutor(observed).evaluateTuple( + PREFIX + "SELECT ?s { " + BROAD_THEN_SELECTIVE + " }")) { + assertEquals("urn:bench:s42", rows.next().getValue("s").stringValue()); + assertFalse(rows.hasNext()); + } + } + + @Test + void singlePatternAskDoesNotRequestStatistics() { + QueryOperations operations = mock(QueryOperations.class, delegatesTo(storage.queries())); + StorageManager observed = mock(StorageManager.class, delegatesTo(storage)); + when(observed.queries()).thenReturn(operations); + doThrow(UnsupportedOperationException.class).when(observed).openReadSnapshot(); + assertTrue(new SparqlQueryExecutor(observed).evaluateBoolean("ASK { ?s ?p ?o }")); + verify(operations, never()).count(any()); + } + + @Test + void emptyStorageProducesStablePlansAndEmptyResults() { + storage.mutations().clear(); + Query query = query(BROAD_THEN_SELECTIVE); + query.complete(new StorageManagerProducer(storage)); + assertEquals(List.of("urn:bench:type", "urn:bench:name"), predicates(query)); + try (var result = new SparqlQueryExecutor(storage).evaluateTuple( + PREFIX + "SELECT * { " + BROAD_THEN_SELECTIVE + " }")) { + assertFalse(result.hasNext()); + } + } + + @Test + void bindKeepsItsOriginalScopeBeforeLaterVariablesBecomeBound() { + try (var rows = new SparqlQueryExecutor(storage).evaluateTuple(PREFIX + + "SELECT ?s ?x { ?s :type :Person BIND(?later AS ?x) ?s :name ?later }")) { + int count = 0; + while (rows.hasNext()) { + assertFalse(rows.next().hasBinding("x")); + count++; + } + assertEquals(200, count); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "?s :type :Person OPTIONAL { ?s :missing ?x } ?s :name \"name42\"", + "?s :type :Person MINUS { ?s :name \"name41\" } ?s :name \"name42\"", + "?s :type :Person . ?s :name ?name FILTER(?name = \"name42\")", + "{ SELECT ?s { ?s :type :Person . ?s :name \"name42\" } } ?s :group ?g" + }) + void semanticOperatorsPreserveSolutions(String body) { + try (var rows = new SparqlQueryExecutor(storage).evaluateTuple(PREFIX + "SELECT ?s { " + body + " }")) { + assertEquals("urn:bench:s42", rows.next().getValue("s").stringValue()); + assertFalse(rows.hasNext()); + } + } + + @Test + void valuesAndUnionPreserveSolutionBags() { + try (var rows = new SparqlQueryExecutor(storage).evaluateTuple(PREFIX + + "SELECT ?s { VALUES ?s { :s42 :s42 } " + BROAD_THEN_SELECTIVE + " }")) { + assertEquals(2, rows.stream().count()); + } + try (var rows = new SparqlQueryExecutor(storage).evaluateTuple(PREFIX + + "SELECT ?s { { " + BROAD_THEN_SELECTIVE + " } UNION { " + BROAD_THEN_SELECTIVE + " } }")) { + assertEquals(2, rows.stream().count()); + } + } + + @Test + void pathsAreBarriersAndDoNotReceiveOrdinaryTripleCounts() { + Query query = query("?s :type :Person . ?s :knows/:knows ?friend . ?s :name \"name42\""); + QueryOperations operations = mock(QueryOperations.class, delegatesTo(storage.queries())); + StorageManager observed = mock(StorageManager.class); + when(observed.queries()).thenReturn(operations); + doThrow(UnsupportedOperationException.class).when(observed).openReadSnapshot(); + query.complete(new StorageManagerProducer(observed)); + assertInstanceOf(PropertyPathEdge.class, edges(query.getBody()).get(1)); + verify(operations, never()).count(any()); + } + + @Test + void estimatesHonorDefaultNamedAndExplicitEmptyDatasets() { + storage.mutations().add(Values.factory().createStatement(iri("s42"), iri("name"), + Values.factory().createLiteral("name42"), iri("g"))); + StorageManagerProducer producer = new StorageManagerProducer(storage); + Query query = query("?s :name \"name42\""); + Edge edge = edges(query.getBody()).getFirst(); + assertEquals(1, producer.estimateCardinality(query, null, edge)); + + query.setDatasetSpecified(true); + query.setNamed(List.of(NodeImpl.forValue(iri("g")))); + assertEquals(0, producer.estimateCardinality(query, null, edge)); + assertEquals(1, producer.estimateCardinality(query, NodeImpl.forValue(iri("g")), edge)); + assertEquals(0, producer.estimateCardinality(query, NodeImpl.forValue(iri("other")), edge)); + assertEquals(QueryStatistics.UNKNOWN_CARDINALITY, + producer.estimateCardinality(query, NodeImpl.forVariable("g"), edge)); + query.setFrom(List.of(NodeImpl.forValue(iri("g")))); + assertEquals(1, producer.estimateCardinality(query, null, edge)); + } + + @Test + void graphScopeIsRestoredBeforePlanningDefaultGraphPatterns() { + Query query = query("GRAPH :g { ?s :type :Person . ?s :name \"name42\" } " + + "?x :type :Person . ?x :name \"name42\""); + List graphs = new ArrayList<>(); + StatisticsProducer producer = new StatisticsProducer(Map.of()) { + @Override + public long estimateCardinality(Query scope, Node graph, Edge edge) { + graphs.add(graph); + return 1; + } + }; + query.complete(producer); + assertEquals(2, graphs.stream().filter(g -> g != null && g.getLabel().equals("urn:bench:g")).count()); + assertEquals(2, graphs.stream().filter(g -> g == null).count()); + } + + @Test + void statisticsUseTheExistingSessionSnapshot() { + try (var reader = storage.openSession(); var transaction = reader.transactions().beginTransaction()) { + Query query = query("?s :name \"name42\""); + Edge edge = edges(query.getBody()).getFirst(); + StorageManagerProducer producer = new StorageManagerProducer(reader); + assertEquals(1, producer.estimateCardinality(query, null, edge)); + storage.mutations().clear(); + assertEquals(1, producer.estimateCardinality(query, null, edge)); + transaction.rollback(); + assertEquals(0, producer.estimateCardinality(query, null, edge)); + } + } + + @Test + void countFailuresRemainQueryDomainErrorsWithOriginalCause() { + StorageException failure = new StorageException(ErrorCode.QUERY_FAILED, "Statistics failed"); + QueryOperations operations = mock(QueryOperations.class, delegatesTo(storage.queries())); + doThrow(failure).when(operations).count(any()); + StorageManager observed = mock(StorageManager.class, delegatesTo(storage)); + when(observed.queries()).thenReturn(operations); + doThrow(UnsupportedOperationException.class).when(observed).openReadSnapshot(); + var exception = assertThrows(QueryEvaluationException.class, () -> new SparqlQueryExecutor(observed) + .evaluateBoolean(PREFIX + "ASK { " + BROAD_THEN_SELECTIVE + " }")); + assertSame(failure, exception.getCause()); + verify(operations, never()).find(any()); + } + + private static Query query(String body) { + return new CoreseAstQueryBuilder().toNextQuery((SelectQueryAst) new SparqlParser() + .parse(PREFIX + "SELECT * { " + body + " }")); + } + + private static IRI iri(String local) { + return Values.factory().createIRI("urn:bench:" + local); + } + + private static List predicates(Query query) { + return edges(query.getBody()).stream().map(edge -> edge.getEdgeLabel()).toList(); + } + + private static List edges(Exp expression) { + List result = new ArrayList<>(); + if (expression.isEdge()) { + result.add(expression.getEdge()); + } + for (Exp child : expression) { + result.addAll(edges(child)); + } + return result; + } + + private static class StatisticsProducer extends ProducerDefault implements QueryStatistics { + private final Map counts; + + private StatisticsProducer(Map counts) { + this.counts = counts; + } + + @Override + public long estimateCardinality(Query query, Node graph, Edge edge) { + return counts.getOrDefault(edge.getEdgeLabel(), UNKNOWN_CARDINALITY); + } + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/engine/storage/GraphIsolationTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/engine/storage/GraphIsolationTest.java index 4e95f6113..35677f5e3 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/engine/storage/GraphIsolationTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/engine/storage/GraphIsolationTest.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -36,7 +37,7 @@ private static IRI iri(String local) { * then closes both. The {@code setup} lambda populates the model before * the query is executed. */ - private int countRows(ModelSetup setup, String sparql) throws Exception { + private int countRows(Consumer setup, String sparql) { StorageManager storage = MemoryStorageManager.builder().build(); Model model = StorageModels.create(storage); setup.accept(model); @@ -52,7 +53,7 @@ private int countRows(ModelSetup setup, String sparql) throws Exception { } } - private List graphValues(ModelSetup setup) throws Exception { + private List graphValues(Consumer setup) { StorageManager storage = MemoryStorageManager.builder().build(); Model model = StorageModels.create(storage); setup.accept(model); @@ -68,14 +69,9 @@ private List graphValues(ModelSetup setup) throws Exception { } } - @FunctionalInterface - interface ModelSetup { - void accept(Model model) throws Exception; - } - @Test @DisplayName("graph-02: only named graph data — default graph query returns 0 rows") - void namedGraphOnlyDefaultQueryReturnsZero() throws Exception { + void namedGraphOnlyDefaultQueryReturnsZero() { int rows = countRows( model -> model.add(iri("s"), iri("p"), iri("o"), iri("g1")), "SELECT * { ?s ?p ?o }"); @@ -85,7 +81,7 @@ void namedGraphOnlyDefaultQueryReturnsZero() throws Exception { @Test @DisplayName("graph-05: default + named — default graph query returns only default rows") - void defaultAndNamedQueryReturnsOnlyDefault() throws Exception { + void defaultAndNamedQueryReturnsOnlyDefault() { int rows = countRows(model -> { // 2 triples in default graph (null context) model.add(iri("s1"), iri("p"), iri("o1")); @@ -100,7 +96,7 @@ void defaultAndNamedQueryReturnsOnlyDefault() throws Exception { @Test @DisplayName("GRAPH ?g — iterates only named graphs, never the default graph") - void graphVariableIteratesOnlyNamedGraphs() throws Exception { + void graphVariableIteratesOnlyNamedGraphs() { List graphs = graphValues(model -> { // 1 triple in default graph — must NOT appear via GRAPH ?g model.add(iri("s0"), iri("p"), iri("o0")); @@ -114,7 +110,7 @@ void graphVariableIteratesOnlyNamedGraphs() throws Exception { @Test @DisplayName("UNION of default and named graphs produces combined results") - void unionDefaultAndNamedReturnsAll() throws Exception { + void unionDefaultAndNamedReturnsAll() { int rows = countRows(model -> { model.add(iri("s1"), iri("p"), iri("o1")); // default model.add(iri("s2"), iri("p"), iri("o2")); // default @@ -127,7 +123,7 @@ void unionDefaultAndNamedReturnsAll() throws Exception { @Test @DisplayName("JOIN default + GRAPH — only subjects present in both graphs match") - void joinDefaultAndNamedGraphFiltersToCommonSubjects() throws Exception { + void joinDefaultAndNamedGraphFiltersToCommonSubjects() { int rows = countRows(model -> { // subject s1 appears in default AND named graph model.add(iri("s1"), iri("p"), iri("o1")); // default diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/SparqlAstTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/SparqlAstTest.java index 18c057a17..71f0abde1 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/SparqlAstTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/ast/SparqlAstTest.java @@ -19,6 +19,7 @@ * term types (VarAst, IriAst, LiteralAst), TriplePatternAst, BgpAst, GroupGraphPatternAst, QueryAst. * */ +@SuppressWarnings("java:S2187") // JUnit discovers the tests in the @Nested suites below. class SparqlAstTest { // ---------- VarAst ---------- diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExprTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExprTest.java index 87ef677ca..4ad9d6a3b 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExprTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExprTest.java @@ -53,7 +53,6 @@ void variableExpressionMetadata() { assertTrue(varExpr.isVariable()); assertFalse(varExpr.isConstant()); assertTrue(varExpr.getExpList().isEmpty()); - assertNull(varExpr.getArg()); } @Test @@ -62,11 +61,10 @@ void boundExpressionAndFilterView() { BoundAst boundAst = new BoundAst(List.of(new VarAst("y"))); AstBackedExpr boundExpr = new AstBackedExpr(boundAst, compiler); - assertTrue(boundExpr.isBound()); assertNotNull(boundExpr.getFilter()); assertTrue(boundExpr.getFilter().isBound()); assertEquals(List.of("y"), boundExpr.getFilter().getVariables()); - assertEquals(boundAst, ((NextFilterFromAst) boundExpr.getFilter()).getFilterExpression()); + assertEquals(boundAst, ((AstBackedFilter) boundExpr.getFilter()).getFilterExpression()); } @Test @@ -74,7 +72,7 @@ void immutableExpressionsThrowOnModification() { WhereCompiler compiler = new WhereCompiler(); AstBackedExpr expr = new AstBackedExpr(new VarAst("z"), compiler); - assertThrows(UnsupportedOperationException.class, () -> expr.setExp(0, expr)); - assertThrows(UnsupportedOperationException.class, () -> expr.setArg(expr)); + assertThrows(UnsupportedOperationException.class, () -> expr.getExpList().add(expr)); + assertTrue(expr.getExpList().isEmpty()); } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSetTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSetTest.java new file mode 100644 index 000000000..5342f81e4 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSetTest.java @@ -0,0 +1,48 @@ +package fr.inria.corese.core.next.query.impl.sparql.execution; + +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.model.NodeImpl; +import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class CoreseBindingSetTest { + @Test + void publicConstructorDoesNotExposeTheMutableMappingKeySet() { + Mapping mapping = boundMapping(); + var bindings = new CoreseBindingSet(mapping); + assertThrows(UnsupportedOperationException.class, () -> bindings.getBindingNames().clear()); + assertEquals(Set.of("x"), bindings.getBindingNames()); + assertEquals("value", bindings.getValue("x").stringValue()); + assertNotNull(mapping.getValue("x")); + } + + @Test + void unboundProjectionVariablesAreExcludedWithoutExposingASeparateMutableSet() { + var bindings = new CoreseBindingSet(boundMapping(), Set.of("x", "missing")); + assertEquals(Set.of("x"), bindings.getBindingNames()); + assertFalse(bindings.hasBinding("missing")); + assertNull(bindings.getValue("missing")); + assertThrows(UnsupportedOperationException.class, () -> bindings.getBindingNames().add("missing")); + var iterator = bindings.iterator(); + assertEquals("x", iterator.next().name()); + assertFalse(iterator.hasNext()); + } + + @Test + void fullyBoundInternalRowsCanShareTheImmutableProjection() { + Set projection = Set.of("x"); + var bindings = new CoreseBindingSet(boundMapping(), projection); + assertSame(projection, bindings.getBindingNames(), "The per-row fast path must not copy the projection"); + assertThrows(UnsupportedOperationException.class, () -> bindings.getBindingNames().clear()); + } + + private static Mapping boundMapping() { + return Mapping.create(new Node[]{NodeImpl.forVariable("x")}, + new Node[]{NodeImpl.forValue(Values.factory().createLiteral("value"))}); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/PipelineTestSupport.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/PipelineTestSupport.java index d64d94279..b3379131e 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/PipelineTestSupport.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/PipelineTestSupport.java @@ -25,13 +25,13 @@ abstract class PipelineTestSupport { ValueFactory valueFactory; MemoryStorageManager storage; - NextSparqlPipelineExecutor executor; + SparqlQueryExecutor executor; @BeforeEach void setUp() { valueFactory = Values.factory(); storage = MemoryStorageManager.builder().build(); - executor = new NextSparqlPipelineExecutor(storage); + executor = new SparqlQueryExecutor(storage); insert(iri(ALICE), iri(KNOWS), iri(BOB)); } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/PreparedPlanCacheTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/PreparedPlanCacheTest.java new file mode 100644 index 000000000..b7d26c007 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/PreparedPlanCacheTest.java @@ -0,0 +1,331 @@ +package fr.inria.corese.core.next.query.impl.sparql.execution; + +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.data.api.term.IRI; +import fr.inria.corese.core.next.query.Repositories; +import fr.inria.corese.core.next.query.api.TupleQuery; +import fr.inria.corese.core.next.query.api.dataset.Dataset; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecution; +import fr.inria.corese.core.next.query.api.result.TupleQueryResult; +import fr.inria.corese.core.next.query.api.repository.Repository; +import fr.inria.corese.core.next.storage.api.StorageManager; +import fr.inria.corese.core.next.storage.api.operations.QueryOperations; +import fr.inria.corese.core.next.storage.impl.memory.MemoryStorageManager; +import fr.inria.corese.core.next.storage.impl.memory.StorageBenchmark; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class PreparedPlanCacheTest { + private static final String PREFIX = "PREFIX : "; + private static final String SELECT = PREFIX + "SELECT ?s { ?s :type :Person . ?s :name \"name42\" }"; + private MemoryStorageManager storage; + private QueryOperations reads; + private Repository repository; + + @BeforeEach + void setUp() { + storage = MemoryStorageManager.builder().build(); + storage.mutations().addAll(StorageBenchmark.data(200, 0, false)); + reads = mock(QueryOperations.class, delegatesTo(storage.queries())); + StorageManager observed = mock(StorageManager.class, delegatesTo(storage)); + when(observed.queries()).thenReturn(reads); + doThrow(UnsupportedOperationException.class).when(observed).openReadSnapshot(); + when(observed.openSession()).thenAnswer(call -> { + StorageManager session = mock(StorageManager.class, delegatesTo(storage.openSession())); + when(session.queries()).thenReturn(reads); + doThrow(UnsupportedOperationException.class).when(session).openReadSnapshot(); + return session; + }); + repository = Repositories.create(observed); + } + + @AfterEach + void tearDown() { + repository.close(); + } + + @Test + void warmEvaluationReusesThePlanWithoutCountsAndKeepsItsEstimates() { + try (var connection = repository.getConnection()) { + TupleQuery query = connection.prepareTupleQuery(SELECT); + var cold = query.evaluateWithDiagnostics(); + assertEquals(List.of("urn:bench:s42"), subjects(cold)); + assertFalse(cold.profile().planReused()); + assertTrue(cold.profile().compilationNanos() > 0); + assertTrue(cold.profile().planningNanos() > 0); + clearInvocations(reads); + var warm = query.evaluateWithDiagnostics(); + assertEquals(List.of("urn:bench:s42"), subjects(warm)); + assertTrue(warm.profile().planReused()); + assertEquals(0, warm.profile().compilationNanos()); + assertEquals(0, warm.profile().planningNanos()); + assertTrue(warm.profile().executionSetupNanos() > 0); + assertEquals(cold.profile().patterns(), warm.profile().patterns()); + verify(reads, never()).count(any()); + verify(reads, times(2)).find(any()); + } + } + + @Test + void normalAndDiagnosticCallsShareOnlyTheCompiledImage() { + try (var connection = repository.getConnection()) { + TupleQuery query = connection.prepareTupleQuery(SELECT); + try (var result = query.evaluate()) { assertEquals(1, result.stream().count()); } + var warm = query.evaluateWithDiagnostics(); + assertTrue(warm.profile().planReused()); + assertEquals(1, subjects(warm).size()); + try (var result = query.evaluate()) { assertEquals(1, result.stream().count()); } + assertEquals(1, warm.profile().patterns().getFirst().candidateEdges()); + verify(reads, times(2)).count(any()); + } + } + + @Test + void bindingAndTimeoutChangesDoNotRecompileTheUnboundPlan() { + try (var connection = repository.getConnection()) { + TupleQuery query = connection.prepareTupleQuery(PREFIX + + "SELECT ?s { ?s :type :Person . ?s :name ?name }"); + subjects(query.evaluateWithDiagnostics()); + clearInvocations(reads); + query.setBinding("s", iri("s42")); + query.setTimeout(Duration.ofSeconds(5)); + var bound = query.evaluateWithDiagnostics(); + assertTrue(bound.profile().planReused()); + assertEquals(List.of("urn:bench:s42"), subjects(bound)); + query.setBinding("s", iri("s43")); + var rebound = query.evaluateWithDiagnostics(); + assertTrue(rebound.profile().planReused()); + assertEquals(List.of("urn:bench:s43"), subjects(rebound)); + query.clearBindings(); + query.setTimeout(Duration.ZERO); + var unbound = query.evaluateWithDiagnostics(); + assertTrue(unbound.profile().planReused()); + assertEquals(200, subjects(unbound).size()); + verify(reads, never()).count(any()); + } + } + + @Test + void equalSizeReplacementInvalidatesThePlan() { + try (var connection = repository.getConnection()) { + TupleQuery query = connection.prepareTupleQuery(SELECT); + subjects(query.evaluateWithDiagnostics()); + long size = connection.size(); + connection.remove(iri("s42"), iri("name"), Values.factory().createLiteral("name42")); + connection.add(iri("s43"), iri("name"), Values.factory().createLiteral("name42")); + assertEquals(size, connection.size()); + clearInvocations(reads); + var updated = query.evaluateWithDiagnostics(); + assertFalse(updated.profile().planReused()); + assertEquals(List.of("urn:bench:s43"), subjects(updated)); + verify(reads, times(2)).count(any()); + } + } + + @Test + void invalidationReordersPatternsUsingTheNewCardinalities() { + try (var connection = repository.getConnection()) { + TupleQuery query = connection.prepareTupleQuery(SELECT); + var before = query.evaluateWithDiagnostics(); + subjects(before); + assertTrue(before.profile().patterns().getFirst().pattern().contains("urn:bench:name")); + var extra = java.util.stream.IntStream.range(0, 250).mapToObj(i -> + Values.factory().createStatement(iri("extra" + i), iri("name"), + Values.factory().createLiteral("name42"))).toList(); + connection.add(extra); + var after = query.evaluateWithDiagnostics(); + assertFalse(after.profile().planReused()); + assertTrue(after.profile().patterns().getFirst().pattern().contains("urn:bench:type")); + assertEquals(200, after.profile().patterns().getFirst().estimatedCardinality()); + assertEquals(251, after.profile().patterns().getLast().estimatedCardinality()); + assertEquals(List.of("urn:bench:s42"), subjects(after)); + } + } + + @Test + void duplicateAndAbsentMutationsDoNotInvalidate() { + try (var connection = repository.getConnection()) { + TupleQuery query = connection.prepareTupleQuery(SELECT); + subjects(query.evaluateWithDiagnostics()); + long version = reads.getDataVersion(); + connection.add(iri("s42"), iri("name"), Values.factory().createLiteral("name42")); + connection.remove(iri("absent"), iri("absent"), iri("absent")); + assertEquals(version, reads.getDataVersion()); + var warm = query.evaluateWithDiagnostics(); + assertTrue(warm.profile().planReused()); + assertEquals(1, subjects(warm).size()); + } + } + + @Test + void changedOrderedDatasetInvalidatesButAnEquivalentDatasetDoesNot() { + try (var connection = repository.getConnection()) { + connection.add(iri("x"), iri("name"), Values.factory().createLiteral("name42"), iri("g1")); + connection.add(iri("x"), iri("type"), iri("Person"), iri("g1")); + TupleQuery query = connection.prepareTupleQuery(SELECT); + subjects(query.evaluateWithDiagnostics()); + query.setDataset(Dataset.builder().defaultGraph(iri("g1")).defaultGraph(iri("g2")).build()); + var changed = query.evaluateWithDiagnostics(); + assertFalse(changed.profile().planReused()); + assertEquals(List.of("urn:bench:x"), subjects(changed)); + query.setDataset(Dataset.builder().defaultGraph(iri("g1")).defaultGraph(iri("g2")).build()); + var equivalent = query.evaluateWithDiagnostics(); + assertTrue(equivalent.profile().planReused()); + subjects(equivalent); + query.setDataset(Dataset.builder().defaultGraph(iri("g2")).defaultGraph(iri("g1")).build()); + var reversed = query.evaluateWithDiagnostics(); + assertFalse(reversed.profile().planReused()); + subjects(reversed); + query.setDataset(Dataset.empty()); + var empty = query.evaluateWithDiagnostics(); + assertFalse(empty.profile().planReused()); + assertTrue(subjects(empty).isEmpty()); + query.setDataset(null); + var original = query.evaluateWithDiagnostics(); + assertFalse(original.profile().planReused()); + assertEquals(List.of("urn:bench:s42"), subjects(original)); + } + } + + @Test + void unavailableVersionNeverEnablesUnsafeReuse() { + when(reads.getDataVersion()).thenReturn(-1L); + try (var connection = repository.getConnection()) { + TupleQuery query = connection.prepareTupleQuery(SELECT); + subjects(query.evaluateWithDiagnostics()); + var repeated = query.evaluateWithDiagnostics(); + assertFalse(repeated.profile().planReused()); + assertTrue(repeated.profile().compilationNanos() > 0); + assertTrue(repeated.profile().planningNanos() > 0); + assertEquals(1, subjects(repeated).size()); + verify(reads, times(4)).count(any()); + } + } + + @Test + void aMutationRacingWithCountsCannotPublishThePlanUnderANewVersion() { + AtomicBoolean first = new AtomicBoolean(true); + doAnswer(call -> { + long count = storage.queries().count(call.getArgument(0)); + if (first.getAndSet(false)) { + storage.mutations().add(Values.factory().createStatement(iri("new"), iri("type"), iri("Person"))); + } + return count; + }).when(reads).count(any()); + try (var connection = repository.getConnection()) { + TupleQuery query = connection.prepareTupleQuery(SELECT); + subjects(query.evaluateWithDiagnostics()); + var replanned = query.evaluateWithDiagnostics(); + assertFalse(replanned.profile().planReused()); + subjects(replanned); + var stable = query.evaluateWithDiagnostics(); + assertTrue(stable.profile().planReused()); + subjects(stable); + verify(reads, times(4)).count(any()); + } + } + + @Test + void transactionViewsInvalidateOnOwnWritesRollbackAndOtherCommits() { + // Real sessions are needed here: the observed reads in the other tests target committed data. + try (var real = Repositories.create(storage.openSession()); + var a = real.getConnection(); var b = real.getConnection()) { + TupleQuery qa = a.prepareTupleQuery(SELECT); + TupleQuery qb = b.prepareTupleQuery(SELECT); + subjects(qa.evaluateWithDiagnostics()); + subjects(qb.evaluateWithDiagnostics()); + a.begin(); + var captured = qa.evaluateWithDiagnostics(); + assertTrue(captured.profile().planReused()); + subjects(captured); + b.add(iri("other"), iri("name"), Values.factory().createLiteral("name42")); + b.add(iri("other"), iri("type"), iri("Person")); + var snapshot = qa.evaluateWithDiagnostics(); + assertTrue(snapshot.profile().planReused()); + assertEquals(List.of("urn:bench:s42"), subjects(snapshot)); + a.rollback(); + var visible = qa.evaluateWithDiagnostics(); + assertFalse(visible.profile().planReused()); + assertEquals(2, subjects(visible).size()); + // Bring B's plan up to date before testing invisible uncommitted writes. + subjects(qb.evaluateWithDiagnostics()); + a.begin(); + a.remove(iri("other"), iri("name"), Values.factory().createLiteral("name42")); + var ownWrite = qa.evaluateWithDiagnostics(); + assertFalse(ownWrite.profile().planReused()); + assertEquals(1, subjects(ownWrite).size()); + var outside = qb.evaluateWithDiagnostics(); + assertTrue(outside.profile().planReused()); + assertEquals(2, subjects(outside).size()); + a.commit(); + var committed = qb.evaluateWithDiagnostics(); + assertFalse(committed.profile().planReused()); + assertEquals(1, subjects(committed).size()); + var ownCommit = qa.evaluateWithDiagnostics(); + assertTrue(ownCommit.profile().planReused(), "Own commit publishes the same visible data"); + subjects(ownCommit); + a.begin(); + a.remove(iri("s42"), iri("name"), Values.factory().createLiteral("name42")); + assertTrue(subjects(qa.evaluateWithDiagnostics()).isEmpty()); + a.rollback(); + var rolledBack = qa.evaluateWithDiagnostics(); + assertFalse(rolledBack.profile().planReused()); + assertEquals(1, subjects(rolledBack).size()); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT ?s { ?s ?p ?o } LIMIT 3", + "SELECT ?s { ?s :type/:missing ?o }", + "SELECT ?s { VALUES ?s { :s42 :s42 :s43 } ?s :type :Person OPTIONAL { ?s :missing ?x } }", + "SELECT ?s { { SELECT ?s { ?s :type :Person . ?s :name \"name42\" } } FILTER EXISTS { ?s :type :Person } }", + "SELECT ?s (COUNT(?name) AS ?n) { ?s :type :Person . ?s :name ?name } GROUP BY ?s ORDER BY ?s LIMIT 3", + "SELECT ?s { ?s :type :Person MINUS { ?s :name \"name41\" } } LIMIT 3" + }) + void mutableOperatorStateDoesNotLeakAcrossWarmEvaluations(String body) { + try (var connection = repository.getConnection()) { + TupleQuery query = connection.prepareTupleQuery(PREFIX + body); + List cold = subjects(query.evaluateWithDiagnostics()); + var warm = query.evaluateWithDiagnostics(); + assertTrue(warm.profile().planReused()); + assertEquals(cold, subjects(warm)); + var repeated = query.evaluateWithDiagnostics(); + assertTrue(repeated.profile().planReused()); + assertEquals(cold, subjects(repeated)); + } + } + + @Test + void earlierResultRemainsReadableAfterAnotherEvaluation() { + try (var connection = repository.getConnection()) { + TupleQuery query = connection.prepareTupleQuery(PREFIX + "SELECT ?s { ?s :type :Person } LIMIT 3"); + var first = query.evaluateWithDiagnostics(); + var second = query.evaluateWithDiagnostics(); + assertTrue(second.profile().planReused()); + assertEquals(subjects(second), subjects(first)); + } + } + + private static List subjects(QueryExecution execution) { + try (var result = execution.result()) { + return result.stream().map(row -> row.getValue("s").stringValue()).sorted().toList(); + } + } + + private static IRI iri(String local) { + return Values.factory().createIRI("urn:bench:" + local); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/ProgressiveSelectTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/ProgressiveSelectTest.java new file mode 100644 index 000000000..5753f0a59 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/ProgressiveSelectTest.java @@ -0,0 +1,569 @@ +package fr.inria.corese.core.next.query.impl.sparql.execution; + +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.data.api.model.Statement; +import fr.inria.corese.core.next.query.Repositories; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.exception.QueryTimeoutException; +import fr.inria.corese.core.next.query.api.repository.Repository; +import fr.inria.corese.core.next.query.api.result.TupleQueryResult; +import fr.inria.corese.core.next.storage.api.StorageManager; +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.operations.QueryOperations; +import fr.inria.corese.core.next.storage.impl.memory.MemoryStorageManager; +import fr.inria.corese.core.next.storage.impl.memory.StorageBenchmark; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.Duration; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** Native snapshot and resumable execution contracts, including partial consumption. */ +class ProgressiveSelectTest { + private static final String PREFIX = "PREFIX : "; + private MemoryStorageManager storage; + private Repository repository; + private final AtomicInteger opened = new AtomicInteger(); + private final AtomicInteger released = new AtomicInteger(); + private final AtomicInteger read = new AtomicInteger(); + private final AtomicInteger snapshots = new AtomicInteger(); + private final AtomicInteger closedSnapshots = new AtomicInteger(); + private final AtomicReference openingFailure = new AtomicReference<>(); + private final AtomicReference closingFailure = new AtomicReference<>(); + private Consumer visit = statement -> { }; + private final CountDownLatch viewClosed = new CountDownLatch(1); + + @BeforeEach + void setUp() { + storage = MemoryStorageManager.builder().build(); + storage.mutations().addAll(StorageBenchmark.data(200, 0, false)); + StorageManager observed = observe(storage); + when(observed.openSession()).thenAnswer(call -> observe(storage.openSession())); + repository = Repositories.create(observed); + } + + private StorageManager observe(StorageManager source) { + StorageManager observed = mock(StorageManager.class, delegatesTo(source)); + doAnswer(call -> { + StorageManager snapshot = source.openReadSnapshot(); + snapshots.incrementAndGet(); + QueryOperations queries = mock(QueryOperations.class, delegatesTo(snapshot.queries())); + doAnswer(find -> { + if (openingFailure.get() != null) throw openingFailure.get(); + opened.incrementAndGet(); + return snapshot.queries().find(find.getArgument(0)).peek(statement -> { + read.incrementAndGet(); + visit.accept(statement); + }).onClose(() -> { + released.incrementAndGet(); + if (closingFailure.get() != null) throw closingFailure.get(); + }); + }).when(queries).find(any()); + StorageManager view = mock(StorageManager.class, delegatesTo(snapshot)); + when(view.queries()).thenReturn(queries); + doAnswer(close -> { + snapshot.close(); + closedSnapshots.incrementAndGet(); + viewClosed.countDown(); + return null; + }).when(view).close(); + return view; + }).when(observed).openReadSnapshot(); + return observed; + } + + @AfterEach + void tearDown() { + repository.close(); + assertEquals(opened.get(), released.get(), "Statement cursors must all be released"); + assertEquals(snapshots.get(), closedSnapshots.get(), "Read snapshots must all be released"); + } + + @Test + void evaluateDoesNotReadAndHasNextBuffersExactlyOneRow() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }").evaluateWithDiagnostics(); + var before = execution.profile(); + assertEquals(0, read.get()); + assertEquals(0, opened.get()); + assertEquals(0, before.patterns().getFirst().candidateEdges()); + try (var rows = execution.result()) { + assertTrue(rows.hasNext()); + assertTrue(rows.hasNext()); + assertEquals(1, read.get()); + var first = rows.next(); + assertEquals(1, read.get()); + assertTrue(rows.hasNext()); + assertEquals(2, read.get()); + assertEquals("urn:bench:s0", first.getValue("s").stringValue()); + } + assertEquals(0, before.patterns().getFirst().candidateEdges(), "Profiles are immutable snapshots"); + assertEquals(2, execution.profile().patterns().getFirst().candidateEdges()); + assertEquals(1, closedSnapshots.get()); + } + } + + @ParameterizedTest + @ValueSource(ints = {1, 10}) + void consumerLimitReadsOnlyTheRequestedRows(int limit) { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }").evaluate()) { + assertEquals(limit, rows.stream().limit(limit).count()); + assertEquals(limit, read.get()); + assertEquals(0, closedSnapshots.get()); + } + assertEquals(1, closedSnapshots.get()); + } + + @Test + void closingBeforeTheFirstRowNeverOpensAStatementCursor() { + try (var connection = repository.getConnection()) { + var rows = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }").evaluate(); + rows.close(); + rows.close(); + assertEquals(0, opened.get()); + assertEquals(1, closedSnapshots.get()); + assertThrows(IllegalStateException.class, rows::hasNext); + } + } + + @Test + void sqlLimitReleasesWithoutAnExtraHasNextCall() { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery("SELECT * { ?s ?p ?o } LIMIT 1").evaluate()) { + assertNotNull(rows.next()); + assertEquals(1, closedSnapshots.get()); + assertEquals(1, released.get()); + assertFalse(rows.hasNext()); + assertThrows(NoSuchElementException.class, rows::next); + } + } + + @Test + void exhaustionReleasesEveryCursorAndTheQuerySnapshot() { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }").evaluate()) { + assertEquals(1200, rows.stream().count()); + assertFalse(rows.hasNext()); + assertEquals(1, closedSnapshots.get()); + assertEquals(opened.get(), released.get()); + } + } + + @Test + void selectiveBgpStopsAtItsFirstJoinedRow() { + try (var connection = repository.getConnection()) { + var execution = connection.prepareTupleQuery(PREFIX + + "SELECT ?s ?name { ?s :type :Person . ?s :name ?name }").evaluateWithDiagnostics(); + try (var rows = execution.result()) { + assertEquals(0, read.get()); + assertEquals("name0", rows.next().getValue("name").stringValue()); + assertEquals(2, read.get()); + } + assertEquals(List.of(1L, 1L), execution.profile().patterns().stream() + .map(pattern -> pattern.candidateEdges()).toList()); + } + } + + @Test + void theSnapshotCoversLookupsOpenedAfterMutations() { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery(PREFIX + + "SELECT ?s ?name { ?s :type :Person . ?s :name ?name }").evaluate()) { + storage.mutations().clear(); + assertEquals("name0", rows.next().getValue("name").stringValue()); + assertEquals(200, 1 + rows.stream().count()); + assertEquals(0, storage.queries().count(fr.inria.corese.core.next.storage.api.model.StatementPattern.matchAll())); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void sourceTransactionCanFinishWhileItsReadSnapshotRemainsValid(boolean commit) { + try (var connection = repository.getConnection()) { + connection.begin(); + connection.add(Values.factory().createIRI("urn:private"), Values.factory().createIRI("urn:p"), Values.factory().createIRI("urn:o")); + try (var rows = connection.prepareTupleQuery("SELECT ?s { ?s ?o }").evaluate()) { + if (commit) connection.commit(); else connection.rollback(); + assertEquals("urn:private", rows.next().getValue("s").stringValue()); + assertFalse(rows.hasNext()); + } + } + } + + @Test + void bindingsAndPreparedPlanFramesAreIndependentForOverlappingCursors() { + try (var connection = repository.getConnection()) { + var query = connection.prepareTupleQuery(PREFIX + "SELECT ?name { ?s :name ?name }"); + query.setBinding("s", Values.factory().createIRI("urn:bench:s0")); + var first = query.evaluateWithDiagnostics(); + query.setBinding("s", Values.factory().createIRI("urn:bench:s1")); + var second = query.evaluateWithDiagnostics(); + try (var left = first.result(); var right = second.result()) { + assertTrue(second.profile().planReused()); + assertEquals(0, second.profile().planningNanos()); + assertEquals("name1", right.next().getValue("name").stringValue()); + assertEquals("name0", left.next().getValue("name").stringValue()); + } + } + } + + @Test + void valuesBindFilterProjectionAndOffsetPreserveSolutions() { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery(""" + SELECT (?n + 1 AS ?value) ?error { + VALUES ?n { 1 2 3 4 5 } + BIND(1 / 0 AS ?error) + FILTER(?n > 1) + } OFFSET 1 LIMIT 2 + """).evaluate()) { + assertEquals(List.of("4", "5"), rows.stream().map(row -> { + assertFalse(row.hasBinding("error")); + assertFalse(row.hasBinding("n")); + return row.getValue("value").stringValue(); + }).toList()); + assertEquals(0, read.get()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT * {}", "SELECT ?x { VALUES ?x { 1 } }"}) + void emptyAndValuesOnlyPatternsProduceTheirSingleSolution(String sparql) { + try (var connection = repository.getConnection(); var rows = connection.prepareTupleQuery(sparql).evaluate()) { + assertNotNull(rows.next()); + assertFalse(rows.hasNext()); + } + } + + @Test + void distinctOffsetAndLimitAreAppliedAfterProjection() { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery("SELECT DISTINCT ?x { VALUES ?x { 1 1 2 2 3 } } OFFSET 1 LIMIT 1") + .evaluate()) { + assertEquals("2", rows.next().getValue("x").stringValue()); + assertEquals(1, closedSnapshots.get()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT * { ?s ?p ?o } LIMIT 0", "SELECT * { ?p ?o }"}) + void noResultsReleaseTheirReadSnapshot(String sparql) { + try (var connection = repository.getConnection(); var rows = connection.prepareTupleQuery(sparql).evaluate()) { + assertFalse(rows.hasNext()); + assertEquals(1, closedSnapshots.get()); + } + } + + @Test + void unionAdvancesOnlyTheCurrentBranchAndPreservesBags() { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery(PREFIX + """ + SELECT ?s { { ?s :type :Person } UNION { ?s :type :Person } } + """).evaluate()) { + assertEquals(0, read.get()); + assertEquals("urn:bench:s0", rows.next().getValue("s").stringValue()); + assertEquals(1, read.get()); + assertEquals(400, 1 + rows.stream().count()); + } + } + + @Test + void namedGraphCursorRetainsTheCapturedRegistryAndStreamsItsRows() { + storage.mutations().addAll(java.util.stream.IntStream.range(0, 200) + .mapToObj(i -> Values.factory().createStatement(Values.factory().createIRI("urn:named:" + i), + Values.factory().createIRI("urn:bench:type"), Values.factory().createIRI("urn:bench:Person"), + Values.factory().createIRI("urn:graph"))).toList()); + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery(PREFIX + "SELECT ?g ?s { GRAPH ?g { ?s :type :Person } }") + .evaluate()) { + assertEquals(0, read.get()); + storage.mutations().clear(); + assertNotNull(rows.next().getValue("g")); + assertEquals(1, read.get()); + assertEquals(200, 1 + rows.stream().count()); + } + } + + @Test + void simpleOptionalStreamsAndKeepsEveryUnmatchedLeftSolution() { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery(PREFIX + """ + SELECT ?s ?name ?missing { + ?s :type :Person OPTIONAL { ?s :name ?name } OPTIONAL { ?s :missing ?missing } + } + """).evaluate()) { + assertEquals(0, read.get()); + var first = rows.next(); + assertEquals("name0", first.getValue("name").stringValue()); + assertFalse(first.hasBinding("missing")); + assertEquals(2, read.get()); + assertEquals(200, 1 + rows.stream().count()); + } + } + + @Test + void groupedPureJoinsRemainProgressive() { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery(PREFIX + + "SELECT ?s ?name { { ?s :type :Person } { ?s :name ?name } }").evaluate()) { + assertEquals(0, read.get()); + assertEquals("name0", rows.next().getValue("name").stringValue()); + assertEquals(2, read.get()); + } + } + + @Test + void scopedExpressionsUseIndependentBindings() { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery(PREFIX + """ + SELECT ?copy { + ?s :type :Person + { BIND(?s AS ?copy) } UNION { BIND(?s AS ?copy) } + } LIMIT 1 + """).evaluate()) { + assertEquals(1, snapshots.get()); + assertEquals(0, read.get()); + assertFalse(rows.next().hasBinding("copy")); + } + } + + @Test + void localExpressionsInUnionGroupsAreProgressiveAndShareTheQueryTimestamp() { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery(PREFIX + """ + SELECT ?s ?time (NOW() AS ?outerTime) { + { ?s :type :Person BIND(NOW() AS ?time) FILTER(BOUND(?s)) } + UNION { ?s :type :Person BIND(NOW() AS ?time) } + } + """).evaluate()) { + assertEquals(0, read.get()); + var row = rows.next(); + assertEquals(row.getValue("time"), row.getValue("outerTime")); + assertEquals(1, read.get()); + } + } + + @Test + void graphNameIsJoinedAfterItsLocalExpressionsHaveBeenEvaluated() { + storage.mutations().createGraph(Values.factory().createIRI("urn:graph")); + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery("SELECT ?g ?copy { GRAPH ?g { BIND(?g AS ?copy) } }").evaluate()) { + assertEquals(1, snapshots.get()); + var row = rows.next(); + assertEquals("urn:graph", row.getValue("g").stringValue()); + assertFalse(row.hasBinding("copy")); + assertFalse(rows.hasNext()); + } + } + + @Test + void scopedJoinsKeepTheirBufferedStrategyToAvoidQuadraticIndependentScans() { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery(PREFIX + """ + SELECT ?s ?name { + { ?s :type :Person } { ?s :name ?name FILTER(BOUND(?name)) } + } + """).evaluate()) { + assertEquals(0, snapshots.get()); + assertEquals(200, rows.stream().count()); + } + } + + @Test + void orderingAndAggregatesKeepTheirBlockingSemantics() { + try (var connection = repository.getConnection()) { + try (var rows = connection.prepareTupleQuery("SELECT (COUNT(*) AS ?count) { ?s ?p ?o }").evaluate()) { + assertEquals("1200", rows.next().getValue("count").stringValue()); + } + try (var rows = connection.prepareTupleQuery(PREFIX + "SELECT ?name { ?s :name ?name } ORDER BY ?name LIMIT 1") + .evaluate()) { + assertEquals("name0", rows.next().getValue("name").stringValue()); + } + assertEquals(1, snapshots.get()); + assertEquals(1, closedSnapshots.get()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void traversalFailureKeepsItsCauseAndReleasesTheCompleteView(boolean duringTraversal) { + StorageException cause = new StorageException(ErrorCode.QUERY_FAILED, "Read failed"); + if (duringTraversal) visit = statement -> { throw cause; }; + else openingFailure.set(cause); + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }").evaluate()) { + var failure = assertThrows(QueryEvaluationException.class, rows::hasNext); + assertSame(cause, failure.getCause()); + assertEquals(1, closedSnapshots.get()); + } + } + + @Test + void fatalTraversalErrorsReleaseTheReadViewWithoutChangingTheirIdentity() { + var cause = new AssertionError("Traversal failed"); + visit = statement -> { throw cause; }; + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }").evaluate()) { + assertSame(cause, assertThrows(AssertionError.class, rows::next)); + assertEquals(1, closedSnapshots.get()); + } + } + + @Test + void closeCallbackFailureIsSuppressedOnTheOriginalTraversalFailure() { + StorageException cause = new StorageException(ErrorCode.QUERY_FAILED, "Read failed"); + closingFailure.set(new IllegalStateException("Close failed")); + visit = statement -> { throw cause; }; + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }").evaluate()) { + var failure = assertThrows(QueryEvaluationException.class, rows::next); + assertSame(cause, failure.getCause()); + assertEquals(List.of(closingFailure.get()), List.of(cause.getSuppressed())); + assertEquals(1, closedSnapshots.get()); + } + } + + @Test + void interruptionBeforeAdvancingPreservesTheFlagAndReleasesTheSnapshot() { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery("SELECT ?x { VALUES ?x { 1 2 3 } }").evaluate()) { + Thread.currentThread().interrupt(); + assertThrows(QueryEvaluationException.class, rows::next); + assertTrue(Thread.currentThread().isInterrupted()); + assertEquals(1, closedSnapshots.get()); + } finally { + Thread.interrupted(); + } + } + + @Test + void interruptionDuringTraversalReleasesResourcesAndPreservesTheInterrupt() { + visit = statement -> Thread.currentThread().interrupt(); + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }").evaluate()) { + assertThrows(QueryEvaluationException.class, rows::hasNext); + assertTrue(Thread.currentThread().isInterrupted()); + assertEquals(1, closedSnapshots.get()); + } finally { + Thread.interrupted(); + } + } + + @Test + void activeTimeoutReleasesResourcesAndCannotInterruptTheFollowingQuery() { + visit = statement -> { + try { + new CountDownLatch(1).await(); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + } + }; + try (var connection = repository.getConnection()) { + var query = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }"); + query.setTimeout(Duration.ofMillis(150)); + try (var rows = query.evaluate()) { + assertThrows(QueryTimeoutException.class, rows::hasNext); + assertEquals(1, closedSnapshots.get()); + assertFalse(Thread.currentThread().isInterrupted()); + } + visit = statement -> { }; + query.setTimeout(Duration.ZERO); + try (var rows = query.evaluate()) { + assertNotNull(rows.next()); + assertFalse(Thread.currentThread().isInterrupted()); + } + } + } + + @Test + void idleTimeoutReleasesTheSnapshotWithoutInterruptingClientCode() throws InterruptedException { + try (var connection = repository.getConnection()) { + var query = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }"); + query.setTimeout(Duration.ofMillis(150)); + try (var rows = query.evaluate()) { + assertTrue(viewClosed.await(5, TimeUnit.SECONDS)); + assertEquals(0, opened.get()); + assertFalse(Thread.currentThread().isInterrupted()); + assertThrows(QueryTimeoutException.class, rows::hasNext); + } + } + } + + @Test + void timeoutAfterHasNextDiscardsTheBufferedRowAndReleasesItsStatementCursor() throws InterruptedException { + try (var connection = repository.getConnection()) { + var query = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }"); + query.setTimeout(Duration.ofMillis(150)); + try (var rows = query.evaluate()) { + assertTrue(rows.hasNext()); + assertTrue(viewClosed.await(5, TimeUnit.SECONDS)); + assertEquals(1, released.get()); + assertFalse(Thread.currentThread().isInterrupted()); + assertThrows(QueryTimeoutException.class, rows::next); + } + } + } + + @Test + void crossThreadCloseInterruptsAnActiveReadAndReleasesItsSnapshot() throws InterruptedException { + var entered = new CountDownLatch(1); + visit = statement -> { + entered.countDown(); + try { + new CountDownLatch(1).await(5, TimeUnit.SECONDS); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + } + }; + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }").evaluate()) { + var cancellationFailed = new java.util.concurrent.atomic.AtomicBoolean(); + Thread closer = Thread.ofVirtual().start(() -> { + try { + if (entered.await(5, TimeUnit.SECONDS)) rows.close(); + else cancellationFailed.set(true); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + cancellationFailed.set(true); + } + }); + try { + assertThrows(QueryEvaluationException.class, rows::next); + assertTrue(Thread.currentThread().isInterrupted()); + assertEquals(1, closedSnapshots.get()); + } finally { + Thread.interrupted(); + closer.join(); + } + assertFalse(cancellationFailed.get()); + } + } + + @Test + void crossThreadCloseReleasesAnIdleCursorAndConsumptionRemainsConfined() throws InterruptedException { + try (var connection = repository.getConnection(); + var rows = connection.prepareTupleQuery("SELECT * { ?s ?p ?o }").evaluate()) { + assertNotNull(rows.next()); + var closer = Thread.ofVirtual().start(rows::close); + closer.join(); + assertEquals(1, closedSnapshots.get()); + assertThrows(IllegalStateException.class, rows::hasNext); + } + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/ScalarFunctionsTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/ScalarFunctionsTest.java index 0fd490afc..f9739ad5d 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/ScalarFunctionsTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/ScalarFunctionsTest.java @@ -18,7 +18,7 @@ class ScalarFunctionsTest { @ParameterizedTest @CsvSource({"double,0E1,0", "double,1E0,1", "float,0E1,0", "float,1E0,1"}) void decimalCastDoesNotRewriteItsSourceTerm(String type, String lexical, int expected) { - var executor = new NextSparqlPipelineExecutor(MemoryStorageManager.builder().build()); + var executor = new SparqlQueryExecutor(MemoryStorageManager.builder().build()); try (var result = executor.evaluateTuple(PREFIX + "SELECT ?v (xsd:decimal(?v) AS ?decimal) WHERE { VALUES ?v { '" + lexical + "'^^xsd:" + type + " } }")) { @@ -34,7 +34,7 @@ void decimalCastDoesNotRewriteItsSourceTerm(String type, String lexical, int exp } private BindingSet evaluate(String expressions) { - var executor = new NextSparqlPipelineExecutor(MemoryStorageManager.builder().build()); + var executor = new SparqlQueryExecutor(MemoryStorageManager.builder().build()); try (var result = executor.evaluateTuple(PREFIX + "SELECT " + expressions + " WHERE {}")) { assertTrue(result.hasNext()); var row = result.next(); @@ -117,7 +117,7 @@ void integerFunctionsReturnXsdInteger(String expression) { @Test void iriResolvesAgainstBaseWithoutExpandingPrefixes() { - var executor = new NextSparqlPipelineExecutor(MemoryStorageManager.builder().build()); + var executor = new SparqlQueryExecutor(MemoryStorageManager.builder().build()); try (var result = executor.evaluateTuple("BASE PREFIX ex: SELECT (IRI('relative') AS ?iri) (URI('ex:name') AS ?uri) WHERE {}")) { var row = result.next(); assertEquals("http://example.org/relative", row.getValue("iri").stringValue()); @@ -127,7 +127,7 @@ void iriResolvesAgainstBaseWithoutExpandingPrefixes() { @Test void projectionAliasesJoinOrderAndDoNotLeakAcrossRows() { - var executor = new NextSparqlPipelineExecutor(MemoryStorageManager.builder().build()); + var executor = new SparqlQueryExecutor(MemoryStorageManager.builder().build()); try (var result = executor.evaluateTuple("SELECT (?x + 1 AS ?a) (?a + 1 AS ?b) WHERE { VALUES ?x { 2 1 } } ORDER BY ?a")) { var rows = result.stream().toList(); assertEquals(2, rows.size()); @@ -139,7 +139,7 @@ void projectionAliasesJoinOrderAndDoNotLeakAcrossRows() { @Test void labeledBlankNodesAreFreshForEachSolutionAndEvaluation() { - var executor = new NextSparqlPipelineExecutor(MemoryStorageManager.builder().build()); + var executor = new SparqlQueryExecutor(MemoryStorageManager.builder().build()); String query = "SELECT (BNODE('label') AS ?a) (BNODE('label') AS ?b) WHERE { VALUES ?x { 1 2 } }"; try (var first = executor.evaluateTuple(query); var second = executor.evaluateTuple(query)) { var rows = first.stream().toList(); diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/SparqlQueryExecutorTest.java similarity index 85% rename from src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java rename to src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/SparqlQueryExecutorTest.java index 22135ebac..a9228b8fd 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/SparqlQueryExecutorTest.java @@ -3,20 +3,64 @@ import fr.inria.corese.core.next.data.api.term.Literal; import fr.inria.corese.core.next.query.api.exception.QueryTimeoutException; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.data.api.model.Statement; +import fr.inria.corese.core.next.storage.api.StorageManager; +import fr.inria.corese.core.next.storage.api.exception.StorageException; +import fr.inria.corese.core.next.storage.api.exception.ErrorCode; +import fr.inria.corese.core.next.storage.api.operations.QueryOperations; import fr.inria.corese.core.next.query.api.result.TupleQueryResult; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.AdditionalAnswers.delegatesTo; /** End-to-end core pipeline tests. */ -class NextSparqlPipelineExecutorTest extends PipelineTestSupport { +class SparqlQueryExecutorTest extends PipelineTestSupport { + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void storageFailuresUseQueryExceptionsAndCloseAnyOpenedCursor(boolean duringIteration) { + StorageException failure = new StorageException(ErrorCode.QUERY_FAILED, "Backend read failed"); + AtomicBoolean closed = new AtomicBoolean(); + QueryOperations queries = mock(QueryOperations.class, delegatesTo(storage.queries())); + doAnswer(invocation -> { + if (!duringIteration) { + throw failure; + } + Statement first = valueFactory.createStatement(iri(ALICE), iri(KNOWS), iri(BOB)); + return Stream.concat(Stream.of(first), Stream.generate(() -> { + throw failure; + })).onClose(() -> closed.set(true)); + }).when(queries).find(any()); + StorageManager failing = mock(StorageManager.class, delegatesTo(storage)); + when(failing.queries()).thenReturn(queries); + doThrow(UnsupportedOperationException.class).when(failing).openReadSnapshot(); + + QueryEvaluationException result = assertThrows(QueryEvaluationException.class, + () -> new SparqlQueryExecutor(failing).evaluateTuple("SELECT ?s WHERE { ?s ?p ?o }")); + + assertSame(failure, result.getCause()); + assertEquals(duringIteration, closed.get()); + assertTrue(executor.evaluateBoolean("ASK { ?s ?p ?o }")); + } @Test @DisplayName("SELECT * WHERE { ?s ?p ?o } runs through the next pipeline") diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/TopKLifecycleTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/TopKLifecycleTest.java new file mode 100644 index 000000000..088d8c86a --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/TopKLifecycleTest.java @@ -0,0 +1,204 @@ +package fr.inria.corese.core.next.query.impl.sparql.execution; + +import fr.inria.corese.core.next.data.api.model.Statement; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.exception.QueryTimeoutException; +import fr.inria.corese.core.next.query.api.result.TupleQueryResult; +import fr.inria.corese.core.next.storage.api.StorageManager; +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.model.StatementPattern; +import fr.inria.corese.core.next.storage.api.operations.QueryOperations; +import fr.inria.corese.core.next.storage.impl.memory.MemoryStorageManager; +import fr.inria.corese.core.next.storage.impl.memory.StorageBenchmark; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** Online selection owns the complete read snapshot until scan and ranking finish. */ +class TopKLifecycleTest { + private MemoryStorageManager storage; + private StorageManager backend; + private SparqlQueryExecutor executor; + private final AtomicInteger read = new AtomicInteger(); + private final AtomicInteger opened = new AtomicInteger(); + private final AtomicInteger released = new AtomicInteger(); + private final AtomicInteger snapshots = new AtomicInteger(); + private final AtomicInteger closedSnapshots = new AtomicInteger(); + private Consumer visit = statement -> { }; + private final AtomicReference openingFailure = new AtomicReference<>(); + private final AtomicReference closingFailure = new AtomicReference<>(); + + @BeforeEach + void setUp() { + storage = MemoryStorageManager.builder().build(); + storage.mutations().addAll(StorageBenchmark.data(200, 0, false)); + backend = mock(StorageManager.class, delegatesTo(storage)); + doAnswer(call -> observeSnapshot()).when(backend).openReadSnapshot(); + executor = new SparqlQueryExecutor(backend); + } + + private StorageManager observeSnapshot() { + StorageManager snapshot = storage.openReadSnapshot(); + snapshots.incrementAndGet(); + QueryOperations reads = mock(QueryOperations.class, delegatesTo(snapshot.queries())); + doAnswer(call -> { + if (openingFailure.get() != null) throw openingFailure.get(); + opened.incrementAndGet(); + return snapshot.queries().find(call.getArgument(0)).peek(statement -> { + read.incrementAndGet(); + visit.accept(statement); + }).onClose(() -> { + released.incrementAndGet(); + if (closingFailure.get() != null) throw closingFailure.get(); + }); + }).when(reads).find(any()); + StorageManager view = mock(StorageManager.class, delegatesTo(snapshot)); + when(view.queries()).thenReturn(reads); + doAnswer(call -> { + snapshot.close(); + closedSnapshots.incrementAndGet(); + return null; + }).when(view).close(); + return view; + } + + @AfterEach + void tearDown() { + storage.close(); + assertEquals(opened.get(), released.get(), "All statement cursors must be released"); + assertEquals(snapshots.get(), closedSnapshots.get(), "All read snapshots must be released"); + } + + @Test + void evaluatesEveryCandidateAndReleasesTheSnapshotBeforeReturningTheSmallResult() { + try (var rows = executor.evaluateTuple("SELECT * { ?s ?p ?o } ORDER BY ?s LIMIT 10")) { + assertEquals(1200, read.get()); + assertEquals(1, closedSnapshots.get()); + assertEquals(10, rows.stream().count()); + } + } + + @Test + void zeroLimitDoesNotReadInputEvenWithOffset() { + try (var rows = executor.evaluateTuple("SELECT * { ?s ?p ?o } ORDER BY ?s OFFSET 100 LIMIT 0")) { + assertFalse(rows.hasNext()); + assertEquals(0, read.get()); + assertEquals(1, closedSnapshots.get()); + } + } + + @Test + void clearingTheSourceDuringScanningPreservesBothTheScanAndLaterJoinLookups() { + visit = statement -> { + if (read.get() == 1) storage.mutations().clear(); + }; + try (var rows = executor.evaluateTuple(""" + SELECT ?s ?name { ?s ?type . ?s ?name } + ORDER BY ?name LIMIT 3 + """)) { + assertEquals(List.of("name0", "name1", "name10"), + rows.stream().map(row -> row.getValue("name").stringValue()).toList()); + assertEquals(0, storage.queries().count(StatementPattern.matchAll())); + assertEquals(400, read.get()); + assertEquals(1, closedSnapshots.get()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void storageFailuresPreserveTheirCauseAndReleaseTheCompleteView(boolean traversing) { + var cause = new StorageException(ErrorCode.QUERY_FAILED, "Read failed"); + if (traversing) visit = statement -> { throw cause; }; + else openingFailure.set(cause); + QueryEvaluationException failure = assertThrows(QueryEvaluationException.class, + () -> executor.evaluateTuple("SELECT * { ?s ?p ?o } ORDER BY ?s LIMIT 10")); + assertSame(cause, failure.getCause()); + assertEquals(1, closedSnapshots.get()); + } + + @Test + void fatalTraversalErrorsAreRethrownUnchangedAfterCleanup() { + var cause = new AssertionError("Traversal failed"); + visit = statement -> { throw cause; }; + assertSame(cause, assertThrows(AssertionError.class, + () -> executor.evaluateTuple("SELECT * { ?s ?p ?o } ORDER BY ?s LIMIT 10"))); + assertEquals(1, closedSnapshots.get()); + } + + @Test + void aFailingCloseCallbackDoesNotHideTheTraversalFailure() { + var cause = new StorageException(ErrorCode.QUERY_FAILED, "Read failed"); + closingFailure.set(new IllegalStateException("Close failed")); + visit = statement -> { throw cause; }; + var failure = assertThrows(QueryEvaluationException.class, + () -> executor.evaluateTuple("SELECT * { ?s ?p ?o } ORDER BY ?s LIMIT 10")); + assertSame(cause, failure.getCause()); + assertEquals(1, cause.getSuppressed().length); + assertSame(closingFailure.get(), cause.getSuppressed()[0]); + assertEquals(1, closedSnapshots.get()); + } + + @Test + void anInterruptedScanReleasesTheSnapshot() { + visit = statement -> Thread.currentThread().interrupt(); + try { + assertThrows(QueryEvaluationException.class, + () -> executor.evaluateTuple("SELECT * { ?s ?p ?o } ORDER BY ?s LIMIT 10")); + assertEquals(1, closedSnapshots.get()); + } finally { + Thread.interrupted(); + } + } + + @Test + void deadlineCancelsTheOnlineScanAndDoesNotContaminateTheFollowingQuery() { + visit = statement -> { + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new QueryEvaluationException("Read interrupted", failure); + } + }; + assertTimeout(Duration.ofSeconds(3), () -> assertThrows(QueryTimeoutException.class, + () -> executor.evaluateTuple("SELECT * { ?s ?p ?o } ORDER BY ?s LIMIT 10", null, null, 100))); + assertEquals(1, closedSnapshots.get()); + assertFalse(Thread.currentThread().isInterrupted()); + visit = statement -> { }; + try (var rows = executor.evaluateTuple("SELECT * { ?s ?p ?o } ORDER BY ?s LIMIT 1")) { + assertTrue(rows.hasNext()); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT * { ?s ?p ?o } ORDER BY ?s", + "SELECT ?p (COUNT(*) AS ?n) { ?s ?p ?o } GROUP BY ?p ORDER BY ?n LIMIT 2", + "SELECT * { { SELECT ?s { ?s ?p ?o } } } ORDER BY ?s LIMIT 2", + "SELECT ?s { ?s + ?o } ORDER BY ?s LIMIT 2", + "SELECT ?s { ?s ?p ?o MINUS { ?s ?x } } ORDER BY ?s LIMIT 2", + "SELECT * { ?s ?p ?o } ORDER BY ?s OFFSET 2147483647 LIMIT 10" + }) + void inadmissibleSelectionRetainsTheClassicCompleteEvaluator(String query) { + try (TupleQueryResult rows = executor.evaluateTuple(query)) { + assertNotNull(rows); + assertEquals(0, snapshots.get()); + verify(backend, never()).openReadSnapshot(); + } + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/TopKQueryTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/TopKQueryTest.java new file mode 100644 index 000000000..9d60263c3 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/TopKQueryTest.java @@ -0,0 +1,155 @@ +package fr.inria.corese.core.next.query.impl.sparql.execution; + +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.data.api.term.Value; +import fr.inria.corese.core.next.query.Repositories; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.ExecutionMode; +import fr.inria.corese.core.next.query.api.diagnostics.QueryExecutionProfile.FallbackReason; +import fr.inria.corese.core.next.query.api.repository.Repository; +import fr.inria.corese.core.next.query.api.result.TupleQueryResult; +import fr.inria.corese.core.next.storage.api.StorageManager; +import fr.inria.corese.core.next.storage.impl.memory.MemoryStorageManager; +import fr.inria.corese.core.next.storage.impl.memory.StorageBenchmark; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.Mockito.*; + +/** Differential checks retain the established stable sort as the independent reference. */ +class TopKQueryTest { + private MemoryStorageManager storage; + private SparqlQueryExecutor executor; + private SparqlQueryExecutor fullSort; + + @BeforeEach + void setUp() { + storage = MemoryStorageManager.builder().build(); + storage.mutations().addAll(StorageBenchmark.data(200, 0, false)); + executor = new SparqlQueryExecutor(storage); + StorageManager classic = mock(StorageManager.class, delegatesTo(storage)); + doThrow(UnsupportedOperationException.class).when(classic).openReadSnapshot(); + fullSort = new SparqlQueryExecutor(classic); + } + + @AfterEach + void tearDown() { + storage.close(); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT ?x { VALUES ?x { 9 1 4 2 7 3 } } ORDER BY ASC(?x) LIMIT 3", + "SELECT ?x { VALUES ?x { 9 1 4 2 7 3 } } ORDER BY DESC(?x) LIMIT 3", + "SELECT ?x { VALUES ?x { 9 1 4 2 7 3 } } ORDER BY ?x OFFSET 2 LIMIT 3", + "SELECT ?x { VALUES ?x { 9 1 4 2 7 3 } } ORDER BY DESC(?x) OFFSET 2 LIMIT 3", + "SELECT ?x { VALUES ?x { 9 1 4 2 7 3 } } ORDER BY ?x LIMIT 1000", + "SELECT ?x { VALUES ?x { 9 1 4 2 7 3 } } ORDER BY ?x LIMIT 2147483646", + "SELECT ?x { VALUES ?x { 9 1 4 2 7 3 } } ORDER BY ?x OFFSET 50 LIMIT 3", + "SELECT ?x { VALUES ?x { 9 1 4 2 7 3 } } ORDER BY ?x OFFSET 2 LIMIT 0", + "SELECT ?x { VALUES ?x {} } ORDER BY ?x LIMIT 10", + "SELECT ?x ?id { VALUES (?x ?id) { (2 0) (2 1) (1 2) (2 3) (1 4) (2 5) (1 6) } } ORDER BY ?x LIMIT 5", + "SELECT ?x ?id { VALUES (?x ?id) { (2 0) (2 1) (1 2) (2 3) (1 4) (2 5) (1 6) } } ORDER BY DESC(?x) OFFSET 1 LIMIT 4", + "SELECT ?x ?id { VALUES (?x ?id) { (2 0) (2 1) (1 2) (2 3) (1 4) (2 5) (1 6) } } ORDER BY ?x DESC(?id) OFFSET 1 LIMIT 4", + "SELECT DISTINCT ?x { VALUES ?x { 9 1 1 4 2 2 7 3 3 } } ORDER BY ?x OFFSET 1 LIMIT 3", + "SELECT DISTINCT ?x { VALUES (?x ?rank) { (1 8) (1 0) (2 5) (3 2) (3 1) } } ORDER BY ?rank LIMIT 2", + "SELECT DISTINCT (?x + 1 AS ?value) { VALUES ?x { 3 3 2 1 1 0 } } ORDER BY DESC(?value) OFFSET 1 LIMIT 2", + "SELECT ?x ?id { VALUES (?x ?id) { (UNDEF 0) (2 1) (UNDEF 2) (1 3) } } ORDER BY ?x LIMIT 3", + "SELECT ?x ?id { VALUES (?x ?id) { (UNDEF 0) (2 1) (UNDEF 2) (1 3) } } ORDER BY DESC(?x) LIMIT 3", + "PREFIX xsd: SELECT ?x { VALUES ?x { 1 1.0 1E0 \"01\"^^xsd:integer 0 2E0 } } ORDER BY ?x LIMIT 4", + "SELECT ?x { VALUES ?x { 2 \"two\" true UNDEF \"one\" 1 } } ORDER BY ?x LIMIT 5", + "SELECT ?s ?o { ?s ?p ?o } ORDER BY DESC(?s) ?o LIMIT 10 OFFSET 7", + "SELECT ?s { { ?s ?o } UNION { ?s ?o } } ORDER BY ?s LIMIT 10 OFFSET 3", + "SELECT ?s ?missing { ?s ?o OPTIONAL { ?s ?missing } } ORDER BY DESC(?s) LIMIT 10" + }) + void agreesExactlyWithTheExistingComparatorAndStableFullSort(String query) { + assertEquals(rows(fullSort, query), rows(executor, query)); + } + + @Test + void offsetIsAppliedAfterSelectingTheBestLimitPlusOffsetCandidates() { + try (var result = executor.evaluateTuple("SELECT ?x { VALUES ?x { 8 7 6 5 4 3 2 1 } } ORDER BY ?x OFFSET 2 LIMIT 3")) { + assertEquals(List.of("3", "4", "5"), result.stream().map(row -> row.getValue("x").stringValue()).toList()); + } + } + + @Test + void genericTypedNumericStorageValuesSortIdenticallyWithTopKAndTheClassicEvaluator() { + String query = "SELECT ?s ?score { ?s ?score } ORDER BY DESC(?score) ?s LIMIT 10"; + assertEquals(rows(fullSort, query), rows(executor, query)); + } + + @Test + void tiesRemainInArrivalOrderWhenLaterBetterCandidatesEvictHeapMembers() { + try (var result = executor.evaluateTuple(""" + SELECT ?id { VALUES (?key ?id) { (2 0) (2 1) (2 2) (1 3) (2 4) (1 5) (2 6) } } + ORDER BY ?key OFFSET 1 LIMIT 3 + """)) { + assertEquals(List.of("5", "0", "1"), result.stream().map(row -> row.getValue("id").stringValue()).toList()); + } + } + + @Test + void randomizedRepeatedKeysMatchFullSortingAcrossSelectionBoundaries() { + var random = new Random(617); + var values = new StringBuilder(); + for (int i = 0; i < 300; i++) values.append('(').append(random.nextInt(11)).append(' ').append(i).append(") "); + String body = "SELECT ?key ?id { VALUES (?key ?id) { " + values + " } } ORDER BY "; + for (String order : List.of("?key", "DESC(?key)", "?key DESC(?id)")) { + for (int offset : List.of(0, 1, 17, 299, 400)) { + String query = body + order + " OFFSET " + offset + " LIMIT 13"; + assertEquals(rows(fullSort, query), rows(executor, query), query); + } + } + } + + @Test + void preparedSelectionReusesItsPlanAndKeepsFreshParametersAndStorageVersions() { + try (Repository repository = Repositories.create(storage); var connection = repository.getConnection()) { + var prepared = connection.prepareTupleQuery("SELECT ?s { ?s ?p ?o } ORDER BY ?s LIMIT 2"); + prepared.setBinding("p", Values.factory().createIRI("urn:bench:type")); + try (var first = prepared.evaluate()) { + assertEquals(2, first.stream().count()); + } + prepared.setBinding("p", Values.factory().createIRI("urn:bench:name")); + var warm = prepared.evaluateWithDiagnostics(); + try (var result = warm.result()) { + assertEquals(2, result.stream().count()); + assertTrue(warm.profile().planReused()); + assertEquals(0, warm.profile().compilationNanos()); + assertEquals(ExecutionMode.MATERIALIZED, warm.profile().delivery().mode()); + assertEquals(FallbackReason.ORDER_BY, warm.profile().delivery().fallbackReason()); + } + connection.clear(); + var changed = prepared.evaluateWithDiagnostics(); + try (var result = changed.result()) { + assertFalse(result.hasNext()); + assertFalse(changed.profile().planReused()); + } + } + } + + private static List> rows(SparqlQueryExecutor engine, String query) { + try (TupleQueryResult result = engine.evaluateTuple(query)) { + var rows = new ArrayList>(); + result.forEach(row -> { + var bindings = new LinkedHashMap(); + for (String name : result.getBindingNames()) { + if (row.hasBinding(name)) bindings.put(name, row.getValue(name)); + } + rows.add(bindings); + }); + return rows; + } + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/TopKSelectorTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/TopKSelectorTest.java new file mode 100644 index 000000000..15867d6a3 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/TopKSelectorTest.java @@ -0,0 +1,75 @@ +package fr.inria.corese.core.next.query.impl.sparql.execution; + +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.query.impl.engine.eval.Eval; +import fr.inria.corese.core.next.query.impl.engine.event.ProcessVisitor; +import fr.inria.corese.core.next.query.impl.engine.model.Node; +import fr.inria.corese.core.next.query.impl.engine.model.NodeImpl; +import fr.inria.corese.core.next.query.impl.engine.pattern.Query; +import fr.inria.corese.core.next.query.impl.engine.solution.Mapping; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.PriorityQueue; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class TopKSelectorTest { + @Test + void theHeapNeverRetainsMoreThanLimitPlusOffsetAndReleasesDiscardedSlots() throws ReflectiveOperationException { + Query query = Query.create(); + query.setLimit(5); + query.setOffset(2); + query.addOrderBy(NodeImpl.forVariable("key")); + var selector = new TopKSelector(query, null); + PriorityQueue heap = heap(selector); + var input = new ArrayList(); + for (int i = 999; i >= 0; i--) { + Mapping row = row(i); + input.add(row); + selector.offer(row); + assertTrue(heap.size() <= 7); + } + input.sort(Comparator.comparingInt(row -> row.getOrderBy()[0].getDatatypeValue().intValue())); + var selected = selector.finish(); + assertEquals(input.subList(2, 7), selected.getList()); + assertTrue(heap.isEmpty()); + assertNull(selected.getEval()); + } + + @Test + void officialComparatorVisitorOverridesAreUsedForSelectionAndFinalOrdering() { + Query query = Query.create(); + query.setLimit(2); + query.addOrderBy(NodeImpl.forVariable("key")); + Eval eval = mock(Eval.class); + ProcessVisitor visitor = mock(ProcessVisitor.class); + when(eval.getVisitor()).thenReturn(visitor); + when(visitor.compare(eq(eval), anyInt(), any(), any())).thenAnswer(call -> -(int) call.getArgument(1)); + var selector = new TopKSelector(query, eval); + Mapping low = row(1); + Mapping middle = row(2); + Mapping high = row(3); + selector.offer(low); + selector.offer(middle); + selector.offer(high); + assertEquals(java.util.List.of(high, middle), selector.finish().getList()); + verify(visitor, atLeastOnce()).compare(eq(eval), anyInt(), any(), any()); + } + + private static Mapping row(int key) { + Mapping mapping = Mapping.create(); + mapping.setOrderBy(new Node[]{NodeImpl.forValue(Values.factory().createLiteral(key))}); + return mapping; + } + + @SuppressWarnings("java:S3011") // Inspect the private heap only to assert the algorithm's retained-memory bound. + private static PriorityQueue heap(TopKSelector selector) throws ReflectiveOperationException { + var field = TopKSelector.class.getDeclaredField("heap"); + field.setAccessible(true); + return (PriorityQueue) field.get(selector); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/io/serializer/support/AbstractResultSerializerTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/io/serializer/support/AbstractResultSerializerTest.java index e0503459d..988f4c754 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/io/serializer/support/AbstractResultSerializerTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/io/serializer/support/AbstractResultSerializerTest.java @@ -50,7 +50,7 @@ public BindingSet next() { @Override public void close() { - + // The in-memory fixture owns no external resources. } }; } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/io/serializer/support/ResultSerializerTestUtils.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/io/serializer/support/ResultSerializerTestUtils.java index 300915db9..6c58b2fbd 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/io/serializer/support/ResultSerializerTestUtils.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/io/serializer/support/ResultSerializerTestUtils.java @@ -41,7 +41,7 @@ public BindingSet next() { @Override public void close() { - + // The in-memory fixture owns no external resources. } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/io/serializer/xml/XmlResultSerializerOptionsTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/io/serializer/xml/XmlResultSerializerOptionsTest.java index 81b553c76..ea25b0e3e 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/io/serializer/xml/XmlResultSerializerOptionsTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/io/serializer/xml/XmlResultSerializerOptionsTest.java @@ -27,14 +27,15 @@ void xmlSerializerIndentPropertyTest() { ResultSerializer serializer = new XmlTupleResultSerializer(results, options); serializer.write(outputWriter); - String expected = - "\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - "\n"; + String expected = """ + + + + + + + + """; assertEquals( expected.replace("\r\n", "\n"), diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/AbstractSparqlParserFeatureTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/AbstractSparqlParserFeatureTest.java index 2f9ded8f3..a9d565d2b 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/AbstractSparqlParserFeatureTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/AbstractSparqlParserFeatureTest.java @@ -10,7 +10,7 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAstTestSupport; import fr.inria.corese.core.next.query.impl.sparql.ast.WhereClauseQueryAst; -public class AbstractSparqlParserFeatureTest { +public abstract class AbstractSparqlParserFeatureTest { /** * Expected {@link IriAst} for the SPARQL shortcut {@code a} ({@code rdf:type}), as produced by {@link SparqlAstBuilder#iri(String)}. diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlExpressionBuilderTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlExpressionBuilderTest.java index bfb8bfd27..e03761b2b 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlExpressionBuilderTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlExpressionBuilderTest.java @@ -22,11 +22,10 @@ class SparqlExpressionBuilderTest extends AbstractSparqlParserFeatureTest { private SparqlExpressionBuilder expressionBuilder; - private SparqlTermBuilder termBuilder; @BeforeEach void setUp() { - termBuilder = new SparqlTermBuilder(new SparqlParserOptions.Builder().build()); + SparqlTermBuilder termBuilder = new SparqlTermBuilder(); expressionBuilder = new SparqlExpressionBuilder(termBuilder, () -> null); } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlParserSelectQueryTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlParserSelectQueryTest.java index a6869ee3c..9379ad3f3 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlParserSelectQueryTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlParserSelectQueryTest.java @@ -478,15 +478,14 @@ ORDER BY DESC(?s) ?p IsIRI(?o) @DisplayName("Should not parse ORDER BY with a constant") void shouldNotParseOrderByConstant() { SparqlParser parser = newParserDefault(); + String query = """ + SELECT ?s WHERE { + ?s ?p ?o + } + ORDER BY + """; - assertThrows(QuerySyntaxException.class, () -> { - parser.parse(""" - SELECT ?s WHERE { - ?s ?p ?o - } - ORDER BY - """); - }); + assertThrows(QuerySyntaxException.class, () -> parser.parse(query)); } @Test @@ -621,82 +620,88 @@ void shouldParseSelectWithGroupByAndHaving() { @DisplayName("Should accept HAVING on a GROUP BY expression alias") void shouldAcceptHavingUsingGroupByExpressionAlias() { SparqlParser parser = newParserDefault(); - - assertDoesNotThrow(() -> parser.parse(""" + String query = """ SELECT ?key WHERE { ?s ?p ?o } GROUP BY (CONCAT(STR(?s), STR(?o)) AS ?key) HAVING (BOUND(?key)) - """)); + """; + + assertDoesNotThrow(() -> parser.parse(query)); } @Test @DisplayName("Should accept HAVING with an aggregate condition") void shouldAcceptHavingUsingAggregateCondition() { SparqlParser parser = newParserDefault(); - - assertDoesNotThrow(() -> parser.parse(""" + String query = """ SELECT ?s WHERE { ?s ?p ?o } GROUP BY ?s HAVING (COUNT(?o) > 1) - """)); + """; + + assertDoesNotThrow(() -> parser.parse(query)); } @Test @DisplayName("Should accept ORDER BY on a GROUP BY expression alias") void shouldAcceptOrderByUsingGroupByExpressionAlias() { SparqlParser parser = newParserDefault(); - - assertDoesNotThrow(() -> parser.parse(""" + String query = """ SELECT ?key WHERE { ?s ?p ?o } GROUP BY (CONCAT(STR(?s), STR(?o)) AS ?key) ORDER BY ?key - """)); + """; + + assertDoesNotThrow(() -> parser.parse(query)); } @Test @DisplayName("Should accept ORDER BY with an aggregate expression") void shouldAcceptOrderByUsingAggregateExpression() { SparqlParser parser = newParserDefault(); - - assertDoesNotThrow(() -> parser.parse(""" + String query = """ SELECT ?s (COUNT(?o) AS ?count) WHERE { ?s ?p ?o } GROUP BY ?s ORDER BY COUNT(?o) - """)); + """; + + assertDoesNotThrow(() -> parser.parse(query)); } @Test @DisplayName("Should accept ORDER BY on a projected aggregate alias") void shouldAcceptOrderByUsingProjectedAggregateAlias() { SparqlParser parser = newParserDefault(); - - assertDoesNotThrow(() -> parser.parse(""" + String query = """ SELECT ?s (COUNT(?o) AS ?count) WHERE { ?s ?p ?o } GROUP BY ?s ORDER BY ?count - """)); + """; + + assertDoesNotThrow(() -> parser.parse(query)); } @Test @DisplayName("Should accept projection variables visible through OPTIONAL") void shouldAcceptProjectionVisibleThroughOptional() { SparqlParser parser = newParserDefault(); - - assertDoesNotThrow(() -> parser.parse(""" + String query = """ SELECT ?o WHERE { OPTIONAL { ?s ?p ?o } } - """)); + """; + + assertDoesNotThrow(() -> parser.parse(query)); } @Test @@ -745,13 +750,14 @@ void shouldAcceptGroupedSelectConstantProjection() { @DisplayName("Should accept grouped SELECT aggregate projections") void shouldAcceptGroupedSelectAggregateProjection() { SparqlParser parser = newParserDefault(); - - assertDoesNotThrow(() -> parser.parse(""" + String query = """ SELECT ?s (COUNT(?o) AS ?count) WHERE { ?s ?p ?o } GROUP BY ?s - """)); + """; + + assertDoesNotThrow(() -> parser.parse(query)); } @Test @@ -861,7 +867,7 @@ void shouldParseSelectDistinctWithUnionQueryTest() { assertInstanceOf(GroupGraphPatternAst.class, union.left()); assertInstanceOf(GroupGraphPatternAst.class, union.right()); - // Left branch: { ?country wdt:P36 ?city. } + // The left UNION branch contains the country-to-capital pattern. GroupGraphPatternAst leftBranch = union.left(); assertEquals(1, leftBranch.patterns().size()); assertInstanceOf(BgpAst.class, leftBranch.patterns().getFirst()); @@ -872,7 +878,7 @@ void shouldParseSelectDistinctWithUnionQueryTest() { assertEquals("wdt:P36", ((IriAst) simplePredicateTerm(leftTriple)).raw()); assertEquals("city", ((VarAst) leftTriple.object()).name()); - // Right branch: { ?city wdt:P17 ?country. ?city wdt:P31 wd:Q1637706. } → two TriplesBlocks → two BGPs + // The right UNION branch expands into two basic graph patterns. GroupGraphPatternAst rightBranch = union.right(); assertEquals(2, rightBranch.patterns().size()); assertInstanceOf(BgpAst.class, rightBranch.patterns().get(0)); diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlPathBuilderTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlPathBuilderTest.java index dcd496bf1..77a0d899d 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlPathBuilderTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlPathBuilderTest.java @@ -24,7 +24,7 @@ class SparqlPathBuilderTest extends AbstractSparqlParserFeatureTest { @BeforeEach void setUp() { - SparqlTermBuilder terms = new SparqlTermBuilder(new SparqlParserOptions.Builder().build()); + SparqlTermBuilder terms = new SparqlTermBuilder(); pathBuilder = new SparqlPathBuilder(terms); } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlTermBuilderTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlTermBuilderTest.java index bf88c3e71..794fa7468 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlTermBuilderTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/parser/SparqlTermBuilderTest.java @@ -23,7 +23,7 @@ class SparqlTermBuilderTest { @BeforeEach void setUp() { - builder = new SparqlTermBuilder(new SparqlParserOptions.Builder().build()); + builder = new SparqlTermBuilder(); } @Nested 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 db8280e3d..1b37d6117 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 @@ -15,7 +15,7 @@ import fr.inria.corese.core.next.query.impl.engine.spi.Environment; import fr.inria.corese.core.next.query.impl.engine.storage.StorageManagerProducer; import fr.inria.corese.core.next.query.impl.sparql.bridge.CoreseAstQueryBuilder; -import fr.inria.corese.core.next.query.impl.sparql.execution.NextSparqlPipelineExecutor; +import fr.inria.corese.core.next.query.impl.sparql.execution.SparqlQueryExecutor; import fr.inria.corese.core.next.query.impl.sparql.parser.SparqlParser; import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst; import fr.inria.corese.core.next.storage.Storages; @@ -50,11 +50,12 @@ private static final class Fixture implements AutoCloseable { final AtomicInteger read = new AtomicInteger(); final AtomicInteger closed = new AtomicInteger(); Consumer callback = s -> { }; - final NextSparqlPipelineExecutor executor; + final SparqlQueryExecutor executor; Fixture() { root.mutations().addAll(StorageBenchmark.data(20, 0, false)); when(observed.queries()).thenReturn(queries); + when(observed.openReadSnapshot()).thenThrow(UnsupportedOperationException.class); when(observed.metadata()).thenReturn(root.metadata()); when(queries.find(any())).thenAnswer(call -> { opened.incrementAndGet(); @@ -62,7 +63,7 @@ private static final class Fixture implements AutoCloseable { read.incrementAndGet(); callback.accept(s); }).onClose(closed::incrementAndGet); }); - executor = new NextSparqlPipelineExecutor(observed); + executor = new SparqlQueryExecutor(observed); } @SuppressWarnings("java:S3011") diff --git a/src/test/java/fr/inria/corese/core/next/storage/impl/memory/ProducerDeduplicationTest.java b/src/test/java/fr/inria/corese/core/next/storage/impl/memory/ProducerDeduplicationTest.java index cfdfb6530..80fd98833 100644 --- a/src/test/java/fr/inria/corese/core/next/storage/impl/memory/ProducerDeduplicationTest.java +++ b/src/test/java/fr/inria/corese/core/next/storage/impl/memory/ProducerDeduplicationTest.java @@ -12,7 +12,7 @@ import fr.inria.corese.core.next.query.impl.engine.storage.StorageManagerProducer; import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst; import fr.inria.corese.core.next.query.impl.sparql.bridge.CoreseAstQueryBuilder; -import fr.inria.corese.core.next.query.impl.sparql.execution.NextSparqlPipelineExecutor; +import fr.inria.corese.core.next.query.impl.sparql.execution.SparqlQueryExecutor; import fr.inria.corese.core.next.query.impl.sparql.parser.SparqlParser; import fr.inria.corese.core.next.storage.Storages; import fr.inria.corese.core.next.storage.api.StorageManager; @@ -181,7 +181,7 @@ private static StatementSpliterator traversal(Stream stream) { try (var root = storage()) { root.mutations().add(row(0, G)); root.mutations().add(row(0, H)); root.mutations().add(Values.factory().createStatement(new SimpleIRI("urn:s0"), new SimpleIRI("urn:p2"), new SimpleIRI("urn:o"), G)); - var executor = new NextSparqlPipelineExecutor(root); + var executor = new SparqlQueryExecutor(root); try (var result = executor.evaluateTuple("SELECT ?s FROM FROM { ?s ?p ?o }")) { assertEquals(List.of("urn:s0", "urn:s0"), result.stream().map(b -> b.getValue("s").stringValue()).toList()); } diff --git a/src/test/java/fr/inria/corese/core/next/storage/impl/memory/StorageDataVersionTest.java b/src/test/java/fr/inria/corese/core/next/storage/impl/memory/StorageDataVersionTest.java new file mode 100644 index 000000000..6639247ff --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/storage/impl/memory/StorageDataVersionTest.java @@ -0,0 +1,103 @@ +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.Statement; +import fr.inria.corese.core.next.data.api.term.IRI; +import fr.inria.corese.core.next.storage.api.model.StatementPattern; +import org.junit.jupiter.api.Test; + +import java.util.Iterator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class StorageDataVersionTest { + @Test + void graphRegistryChangesInvalidateEvenWhenThereAreNoStatements() { + try (var storage = MemoryStorageManager.builder().build()) { + long empty = storage.queries().getDataVersion(); + storage.mutations().clear(); + assertEquals(empty, storage.queries().getDataVersion()); + assertTrue(storage.mutations().createGraph(iri("graph"))); + long created = storage.queries().getDataVersion(); + assertNotEquals(empty, created); + assertFalse(storage.mutations().createGraph(iri("graph"))); + assertEquals(created, storage.queries().getDataVersion()); + assertTrue(storage.mutations().dropGraph(iri("graph"))); + assertNotEquals(created, storage.queries().getDataVersion()); + } + } + + @Test + void effectiveBatchDeleteAndClearChangesHaveNewTokensButReadsDoNot() { + try (var storage = MemoryStorageManager.builder().build()) { + long empty = storage.queries().getDataVersion(); + storage.mutations().addAll(List.of(statement("a"), statement("b"))); + long loaded = storage.queries().getDataVersion(); + assertNotEquals(empty, loaded); + storage.mutations().addAll(List.of(statement("a"), statement("b"))); + try (var reader = storage.queries().find(StatementPattern.matchAll())) { + assertEquals(2, reader.count()); + } + assertEquals(loaded, storage.queries().getDataVersion()); + assertEquals(1, storage.mutations().remove(StatementPattern.of(iri("a"), null, null))); + long deleted = storage.queries().getDataVersion(); + assertNotEquals(loaded, deleted); + assertEquals(0, storage.mutations().remove(StatementPattern.of(iri("absent"), null, null))); + assertEquals(deleted, storage.queries().getDataVersion()); + storage.mutations().clear(); + assertNotEquals(deleted, storage.queries().getDataVersion()); + } + } + + @Test + void failedStreamingBatchPreservesTheTokenAndData() { + try (var storage = MemoryStorageManager.builder().build()) { + long version = storage.queries().getDataVersion(); + Iterable failing = () -> new Iterator<>() { + private boolean first = true; + @Override public boolean hasNext() { return true; } + @Override public Statement next() { + if (!first) throw new IllegalStateException("Broken source"); + first = false; + return statement("staged"); + } + }; + assertThrows(RuntimeException.class, () -> storage.mutations().addAll(failing)); + assertEquals(version, storage.queries().getDataVersion()); + assertEquals(0, storage.queries().count(StatementPattern.matchAll())); + } + } + + @Test + void transactionVersionsDescribeItsOwnViewAndConflictingCommitsRemainRejected() { + try (var root = MemoryStorageManager.builder().build(); + var a = root.openSession(); var b = root.openSession()) { + long initial = root.queries().getDataVersion(); + try (var transaction = a.transactions().beginTransaction()) { + assertEquals(initial, a.queries().getDataVersion()); + a.mutations().add(statement("uncommitted")); + long ownWrite = a.queries().getDataVersion(); + assertNotEquals(initial, ownWrite); + assertEquals(initial, b.queries().getDataVersion()); + b.mutations().add(statement("committed")); + long committed = b.queries().getDataVersion(); + assertNotEquals(initial, committed); + assertEquals(ownWrite, a.queries().getDataVersion()); + assertThrows(RuntimeException.class, transaction::commit); + assertEquals(ownWrite, a.queries().getDataVersion()); + transaction.rollback(); + assertEquals(committed, a.queries().getDataVersion()); + assertEquals(1, a.queries().count(StatementPattern.matchAll())); + } + } + } + + private static IRI iri(String name) { + return Values.factory().createIRI("urn:version:" + name); + } + + private static Statement statement(String name) { + return Values.factory().createStatement(iri(name), iri("p"), iri("o")); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/storage/impl/memory/StorageReadSnapshotTest.java b/src/test/java/fr/inria/corese/core/next/storage/impl/memory/StorageReadSnapshotTest.java new file mode 100644 index 000000000..740e617b6 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/storage/impl/memory/StorageReadSnapshotTest.java @@ -0,0 +1,70 @@ +package fr.inria.corese.core.next.storage.impl.memory; + +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.storage.api.StorageManager; +import fr.inria.corese.core.next.storage.api.model.StatementPattern; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; + +class StorageReadSnapshotTest { + @Test + void snapshotsCaptureContentsGraphRegistryAndVersionUntilClosed() { + try (var storage = MemoryStorageManager.builder().build()) { + storage.mutations().addAll(StorageBenchmark.data(20, 0, false)); + long version = storage.queries().getDataVersion(); + try (var view = storage.openReadSnapshot()) { + storage.mutations().clear(); + storage.mutations().createGraph(Values.factory().createIRI("urn:new-graph")); + assertEquals(version, view.queries().getDataVersion()); + assertTrue(view.metadata().getContexts().isEmpty()); + assertEquals(120, view.queries().count(StatementPattern.matchAll())); + try (var stream = view.queries().find(StatementPattern.matchAll())) { + assertEquals(120, stream.count()); + } + assertThrows(IllegalStateException.class, () -> view.mutations().clear()); + assertThrows(IllegalStateException.class, () -> view.transactions().beginTransaction()); + try (var nested = view.openReadSnapshot()) { + assertEquals(120, nested.queries().count(StatementPattern.matchAll())); + } + } + assertEquals(0, storage.queries().count(StatementPattern.matchAll())); + } + } + + @Test + void querySnapshotClosureCannotFinishTheSourcesTransaction() { + try (var storage = MemoryStorageManager.builder().build(); var transaction = storage.transactions().beginTransaction()) { + storage.mutations().addAll(StorageBenchmark.data(20, 0, false)); + try (var snapshot = storage.openReadSnapshot()) { + assertEquals(120, snapshot.queries().count(StatementPattern.matchAll())); + } + assertTrue(transaction.isActive()); + assertEquals(120, storage.queries().count(StatementPattern.matchAll())); + transaction.rollback(); + assertEquals(0, storage.queries().count(StatementPattern.matchAll())); + } + } + + @Test + void snapshotReadsAreThreadConfinedButCancellationCanReleaseThemElsewhere() throws InterruptedException { + try (var storage = MemoryStorageManager.builder().build(); StorageManager snapshot = storage.openReadSnapshot()) { + var failure = new AtomicReference(); + var reader = Thread.ofVirtual().start(() -> { + try { + snapshot.queries().count(StatementPattern.matchAll()); + } catch (IllegalStateException expected) { + failure.set(expected); + } + }); + reader.join(); + assertInstanceOf(IllegalStateException.class, failure.get()); + var closer = Thread.ofVirtual().start(snapshot::close); + closer.join(); + assertFalse(snapshot.isOpen()); + assertTrue(storage.isOpen()); + } + } +}