diff options
author | Kristóf Marussy <kristof@marussy.com> | 2023-09-16 13:19:31 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2023-09-16 16:53:01 +0200 |
commit | 97b0c4c1192fe5580a7957c844acc8092b56c604 (patch) | |
tree | bea3cdf9aaeb5da2864fcf87780d356661af8f63 /subprojects/store-query-interpreter/src | |
parent | build: fix Sonar quality gate issues (diff) | |
download | refinery-97b0c4c1192fe5580a7957c844acc8092b56c604.tar.gz refinery-97b0c4c1192fe5580a7957c844acc8092b56c604.tar.zst refinery-97b0c4c1192fe5580a7957c844acc8092b56c604.zip |
chore: remove VIATRA branding
Rename VIATRA subprojects to Refinery Interpreter to avoid interfering with
Eclipse Foundation trademarks.
Uses refering to a specific (historical) version of VIATRA were kept to avoid
ambiguity.
Diffstat (limited to 'subprojects/store-query-interpreter/src')
46 files changed, 5494 insertions, 0 deletions
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/QueryInterpreterAdapter.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/QueryInterpreterAdapter.java new file mode 100644 index 00000000..83e69ae8 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/QueryInterpreterAdapter.java | |||
@@ -0,0 +1,18 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter; | ||
7 | |||
8 | import tools.refinery.store.query.ModelQueryAdapter; | ||
9 | import tools.refinery.store.query.interpreter.internal.QueryInterpreterBuilderImpl; | ||
10 | |||
11 | public interface QueryInterpreterAdapter extends ModelQueryAdapter { | ||
12 | @Override | ||
13 | QueryInterpreterStoreAdapter getStoreAdapter(); | ||
14 | |||
15 | static QueryInterpreterBuilder builder() { | ||
16 | return new QueryInterpreterBuilderImpl(); | ||
17 | } | ||
18 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/QueryInterpreterBuilder.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/QueryInterpreterBuilder.java new file mode 100644 index 00000000..6e167d0d --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/QueryInterpreterBuilder.java | |||
@@ -0,0 +1,54 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter; | ||
7 | |||
8 | import tools.refinery.store.model.ModelStore; | ||
9 | import tools.refinery.store.query.ModelQueryBuilder; | ||
10 | import tools.refinery.store.query.dnf.AnyQuery; | ||
11 | import tools.refinery.store.query.dnf.Dnf; | ||
12 | import tools.refinery.store.query.rewriter.DnfRewriter; | ||
13 | import tools.refinery.interpreter.api.InterpreterEngineOptions; | ||
14 | import tools.refinery.interpreter.matchers.backend.IQueryBackendFactory; | ||
15 | import tools.refinery.interpreter.matchers.backend.QueryEvaluationHint; | ||
16 | |||
17 | import java.util.Collection; | ||
18 | import java.util.function.Function; | ||
19 | |||
20 | @SuppressWarnings("UnusedReturnValue") | ||
21 | public interface QueryInterpreterBuilder extends ModelQueryBuilder { | ||
22 | QueryInterpreterBuilder engineOptions(InterpreterEngineOptions engineOptions); | ||
23 | |||
24 | QueryInterpreterBuilder defaultHint(QueryEvaluationHint queryEvaluationHint); | ||
25 | |||
26 | QueryInterpreterBuilder backend(IQueryBackendFactory queryBackendFactory); | ||
27 | |||
28 | QueryInterpreterBuilder cachingBackend(IQueryBackendFactory queryBackendFactory); | ||
29 | |||
30 | QueryInterpreterBuilder searchBackend(IQueryBackendFactory queryBackendFactory); | ||
31 | |||
32 | @Override | ||
33 | default QueryInterpreterBuilder queries(AnyQuery... queries) { | ||
34 | ModelQueryBuilder.super.queries(queries); | ||
35 | return this; | ||
36 | } | ||
37 | |||
38 | @Override | ||
39 | default QueryInterpreterBuilder queries(Collection<? extends AnyQuery> queries) { | ||
40 | ModelQueryBuilder.super.queries(queries); | ||
41 | return this; | ||
42 | } | ||
43 | |||
44 | @Override | ||
45 | QueryInterpreterBuilder query(AnyQuery query); | ||
46 | |||
47 | @Override | ||
48 | QueryInterpreterBuilder rewriter(DnfRewriter rewriter); | ||
49 | |||
50 | QueryInterpreterBuilder computeHint(Function<Dnf, QueryEvaluationHint> computeHint); | ||
51 | |||
52 | @Override | ||
53 | QueryInterpreterStoreAdapter build(ModelStore store); | ||
54 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/QueryInterpreterStoreAdapter.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/QueryInterpreterStoreAdapter.java new file mode 100644 index 00000000..9c55ecc2 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/QueryInterpreterStoreAdapter.java | |||
@@ -0,0 +1,17 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter; | ||
7 | |||
8 | import tools.refinery.interpreter.api.InterpreterEngineOptions; | ||
9 | import tools.refinery.store.model.Model; | ||
10 | import tools.refinery.store.query.ModelQueryStoreAdapter; | ||
11 | |||
12 | public interface QueryInterpreterStoreAdapter extends ModelQueryStoreAdapter { | ||
13 | InterpreterEngineOptions getEngineOptions(); | ||
14 | |||
15 | @Override | ||
16 | QueryInterpreterAdapter createModelAdapter(Model model); | ||
17 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/QueryInterpreterAdapterImpl.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/QueryInterpreterAdapterImpl.java new file mode 100644 index 00000000..ee527fd3 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/QueryInterpreterAdapterImpl.java | |||
@@ -0,0 +1,122 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal; | ||
7 | |||
8 | import tools.refinery.store.model.Model; | ||
9 | import tools.refinery.store.model.ModelListener; | ||
10 | import tools.refinery.store.query.dnf.AnyQuery; | ||
11 | import tools.refinery.store.query.dnf.FunctionalQuery; | ||
12 | import tools.refinery.store.query.dnf.Query; | ||
13 | import tools.refinery.store.query.dnf.RelationalQuery; | ||
14 | import tools.refinery.store.query.resultset.AnyResultSet; | ||
15 | import tools.refinery.store.query.resultset.EmptyResultSet; | ||
16 | import tools.refinery.store.query.resultset.ResultSet; | ||
17 | import tools.refinery.store.query.interpreter.QueryInterpreterAdapter; | ||
18 | import tools.refinery.store.query.interpreter.internal.matcher.InterpretedFunctionalMatcher; | ||
19 | import tools.refinery.store.query.interpreter.internal.matcher.RawPatternMatcher; | ||
20 | import tools.refinery.store.query.interpreter.internal.matcher.InterpretedRelationalMatcher; | ||
21 | import tools.refinery.interpreter.CancellationToken; | ||
22 | import tools.refinery.interpreter.api.AdvancedInterpreterEngine; | ||
23 | import tools.refinery.interpreter.api.GenericQueryGroup; | ||
24 | import tools.refinery.interpreter.api.IQuerySpecification; | ||
25 | |||
26 | import java.util.Collections; | ||
27 | import java.util.LinkedHashMap; | ||
28 | import java.util.Map; | ||
29 | |||
30 | public class QueryInterpreterAdapterImpl implements QueryInterpreterAdapter, ModelListener { | ||
31 | private final Model model; | ||
32 | private final QueryInterpreterStoreAdapterImpl storeAdapter; | ||
33 | private final AdvancedInterpreterEngine queryEngine; | ||
34 | private final Map<AnyQuery, AnyResultSet> resultSets; | ||
35 | private boolean pendingChanges; | ||
36 | |||
37 | QueryInterpreterAdapterImpl(Model model, QueryInterpreterStoreAdapterImpl storeAdapter) { | ||
38 | this.model = model; | ||
39 | this.storeAdapter = storeAdapter; | ||
40 | var scope = new RelationalScope(this); | ||
41 | queryEngine = AdvancedInterpreterEngine.createUnmanagedEngine(scope, | ||
42 | storeAdapter.getEngineOptions()); | ||
43 | |||
44 | var querySpecifications = storeAdapter.getQuerySpecifications(); | ||
45 | GenericQueryGroup.of( | ||
46 | Collections.<IQuerySpecification<?>>unmodifiableCollection(querySpecifications.values()).stream() | ||
47 | ).prepare(queryEngine); | ||
48 | queryEngine.flushChanges(); | ||
49 | var vacuousQueries = storeAdapter.getVacuousQueries(); | ||
50 | resultSets = new LinkedHashMap<>(querySpecifications.size() + vacuousQueries.size()); | ||
51 | for (var entry : querySpecifications.entrySet()) { | ||
52 | var rawPatternMatcher = queryEngine.getMatcher(entry.getValue()); | ||
53 | var query = entry.getKey(); | ||
54 | resultSets.put(query, createResultSet((Query<?>) query, rawPatternMatcher)); | ||
55 | } | ||
56 | for (var vacuousQuery : vacuousQueries) { | ||
57 | resultSets.put(vacuousQuery, new EmptyResultSet<>(this, (Query<?>) vacuousQuery)); | ||
58 | } | ||
59 | |||
60 | model.addListener(this); | ||
61 | } | ||
62 | |||
63 | private <T> ResultSet<T> createResultSet(Query<T> query, RawPatternMatcher matcher) { | ||
64 | if (query instanceof RelationalQuery relationalQuery) { | ||
65 | @SuppressWarnings("unchecked") | ||
66 | var resultSet = (ResultSet<T>) new InterpretedRelationalMatcher(this, relationalQuery, matcher); | ||
67 | return resultSet; | ||
68 | } else if (query instanceof FunctionalQuery<T> functionalQuery) { | ||
69 | return new InterpretedFunctionalMatcher<>(this, functionalQuery, matcher); | ||
70 | } else { | ||
71 | throw new IllegalArgumentException("Unknown query: " + query); | ||
72 | } | ||
73 | } | ||
74 | |||
75 | @Override | ||
76 | public Model getModel() { | ||
77 | return model; | ||
78 | } | ||
79 | |||
80 | @Override | ||
81 | public QueryInterpreterStoreAdapterImpl getStoreAdapter() { | ||
82 | return storeAdapter; | ||
83 | } | ||
84 | |||
85 | public CancellationToken getCancellationToken() { | ||
86 | return storeAdapter.getCancellationToken(); | ||
87 | } | ||
88 | |||
89 | @Override | ||
90 | public <T> ResultSet<T> getResultSet(Query<T> query) { | ||
91 | var canonicalQuery = storeAdapter.getCanonicalQuery(query); | ||
92 | var resultSet = resultSets.get(canonicalQuery); | ||
93 | if (resultSet == null) { | ||
94 | throw new IllegalArgumentException("No matcher for query %s in model".formatted(query.name())); | ||
95 | } | ||
96 | @SuppressWarnings("unchecked") | ||
97 | var typedResultSet = (ResultSet<T>) resultSet; | ||
98 | return typedResultSet; | ||
99 | } | ||
100 | |||
101 | @Override | ||
102 | public boolean hasPendingChanges() { | ||
103 | return pendingChanges; | ||
104 | } | ||
105 | |||
106 | public void markAsPending() { | ||
107 | if (!pendingChanges) { | ||
108 | pendingChanges = true; | ||
109 | } | ||
110 | } | ||
111 | |||
112 | @Override | ||
113 | public void flushChanges() { | ||
114 | queryEngine.flushChanges(); | ||
115 | pendingChanges = false; | ||
116 | } | ||
117 | |||
118 | @Override | ||
119 | public void afterRestore() { | ||
120 | flushChanges(); | ||
121 | } | ||
122 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/QueryInterpreterBuilderImpl.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/QueryInterpreterBuilderImpl.java new file mode 100644 index 00000000..c0d802da --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/QueryInterpreterBuilderImpl.java | |||
@@ -0,0 +1,169 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal; | ||
7 | |||
8 | import tools.refinery.store.adapter.AbstractModelAdapterBuilder; | ||
9 | import tools.refinery.store.model.ModelStore; | ||
10 | import tools.refinery.store.query.dnf.AnyQuery; | ||
11 | import tools.refinery.store.query.dnf.Dnf; | ||
12 | import tools.refinery.store.query.rewriter.CompositeRewriter; | ||
13 | import tools.refinery.store.query.rewriter.DnfRewriter; | ||
14 | import tools.refinery.store.query.rewriter.DuplicateDnfRemover; | ||
15 | import tools.refinery.store.query.rewriter.InputParameterResolver; | ||
16 | import tools.refinery.store.query.interpreter.QueryInterpreterBuilder; | ||
17 | import tools.refinery.store.query.interpreter.internal.localsearch.FlatCostFunction; | ||
18 | import tools.refinery.store.query.interpreter.internal.matcher.RawPatternMatcher; | ||
19 | import tools.refinery.store.query.interpreter.internal.pquery.Dnf2PQuery; | ||
20 | import tools.refinery.interpreter.api.IQuerySpecification; | ||
21 | import tools.refinery.interpreter.api.InterpreterEngineOptions; | ||
22 | import tools.refinery.interpreter.localsearch.matcher.integration.LocalSearchGenericBackendFactory; | ||
23 | import tools.refinery.interpreter.localsearch.matcher.integration.LocalSearchHintOptions; | ||
24 | import tools.refinery.interpreter.matchers.backend.IQueryBackendFactory; | ||
25 | import tools.refinery.interpreter.matchers.backend.QueryEvaluationHint; | ||
26 | import tools.refinery.interpreter.rete.matcher.ReteBackendFactory; | ||
27 | |||
28 | import java.util.*; | ||
29 | import java.util.function.Function; | ||
30 | |||
31 | public class QueryInterpreterBuilderImpl extends AbstractModelAdapterBuilder<QueryInterpreterStoreAdapterImpl> | ||
32 | implements QueryInterpreterBuilder { | ||
33 | private InterpreterEngineOptions.Builder engineOptionsBuilder; | ||
34 | private QueryEvaluationHint defaultHint = new QueryEvaluationHint(Map.of( | ||
35 | // Use a cost function that ignores the initial (empty) model but allows higher arity input keys. | ||
36 | LocalSearchHintOptions.PLANNER_COST_FUNCTION, new FlatCostFunction() | ||
37 | ), (IQueryBackendFactory) null); | ||
38 | private final CompositeRewriter rewriter; | ||
39 | private final Dnf2PQuery dnf2PQuery = new Dnf2PQuery(); | ||
40 | private final Set<AnyQuery> queries = new LinkedHashSet<>(); | ||
41 | |||
42 | public QueryInterpreterBuilderImpl() { | ||
43 | engineOptionsBuilder = new InterpreterEngineOptions.Builder() | ||
44 | .withDefaultBackend(ReteBackendFactory.INSTANCE) | ||
45 | .withDefaultCachingBackend(ReteBackendFactory.INSTANCE) | ||
46 | .withDefaultSearchBackend(LocalSearchGenericBackendFactory.INSTANCE); | ||
47 | rewriter = new CompositeRewriter(); | ||
48 | rewriter.addFirst(new DuplicateDnfRemover()); | ||
49 | rewriter.addFirst(new InputParameterResolver()); | ||
50 | } | ||
51 | |||
52 | @Override | ||
53 | public QueryInterpreterBuilder engineOptions(InterpreterEngineOptions engineOptions) { | ||
54 | checkNotConfigured(); | ||
55 | engineOptionsBuilder = new InterpreterEngineOptions.Builder(engineOptions); | ||
56 | return this; | ||
57 | } | ||
58 | |||
59 | @Override | ||
60 | public QueryInterpreterBuilder defaultHint(QueryEvaluationHint queryEvaluationHint) { | ||
61 | checkNotConfigured(); | ||
62 | defaultHint = defaultHint.overrideBy(queryEvaluationHint); | ||
63 | return this; | ||
64 | } | ||
65 | |||
66 | @Override | ||
67 | public QueryInterpreterBuilder backend(IQueryBackendFactory queryBackendFactory) { | ||
68 | checkNotConfigured(); | ||
69 | engineOptionsBuilder.withDefaultBackend(queryBackendFactory); | ||
70 | return this; | ||
71 | } | ||
72 | |||
73 | @Override | ||
74 | public QueryInterpreterBuilder cachingBackend(IQueryBackendFactory queryBackendFactory) { | ||
75 | checkNotConfigured(); | ||
76 | engineOptionsBuilder.withDefaultCachingBackend(queryBackendFactory); | ||
77 | return this; | ||
78 | } | ||
79 | |||
80 | @Override | ||
81 | public QueryInterpreterBuilder searchBackend(IQueryBackendFactory queryBackendFactory) { | ||
82 | checkNotConfigured(); | ||
83 | engineOptionsBuilder.withDefaultSearchBackend(queryBackendFactory); | ||
84 | return this; | ||
85 | } | ||
86 | |||
87 | @Override | ||
88 | public QueryInterpreterBuilder queries(Collection<? extends AnyQuery> queries) { | ||
89 | checkNotConfigured(); | ||
90 | this.queries.addAll(queries); | ||
91 | return this; | ||
92 | } | ||
93 | |||
94 | @Override | ||
95 | public QueryInterpreterBuilder query(AnyQuery query) { | ||
96 | checkNotConfigured(); | ||
97 | queries.add(query); | ||
98 | return this; | ||
99 | } | ||
100 | |||
101 | @Override | ||
102 | public QueryInterpreterBuilder rewriter(DnfRewriter rewriter) { | ||
103 | this.rewriter.addFirst(rewriter); | ||
104 | return this; | ||
105 | } | ||
106 | |||
107 | @Override | ||
108 | public QueryInterpreterBuilder computeHint(Function<Dnf, QueryEvaluationHint> computeHint) { | ||
109 | checkNotConfigured(); | ||
110 | dnf2PQuery.setComputeHint(computeHint); | ||
111 | return this; | ||
112 | } | ||
113 | |||
114 | @Override | ||
115 | public QueryInterpreterStoreAdapterImpl doBuild(ModelStore store) { | ||
116 | var canonicalQueryMap = new HashMap<AnyQuery, AnyQuery>(); | ||
117 | var querySpecifications = new LinkedHashMap<AnyQuery, IQuerySpecification<RawPatternMatcher>>(); | ||
118 | var vacuousQueries = new LinkedHashSet<AnyQuery>(); | ||
119 | for (var query : queries) { | ||
120 | var canonicalQuery = rewriter.rewrite(query); | ||
121 | canonicalQueryMap.put(query, canonicalQuery); | ||
122 | var dnf = canonicalQuery.getDnf(); | ||
123 | var reduction = dnf.getReduction(); | ||
124 | switch (reduction) { | ||
125 | case NOT_REDUCIBLE -> { | ||
126 | var pQuery = dnf2PQuery.translate(dnf); | ||
127 | querySpecifications.put(canonicalQuery, pQuery.build()); | ||
128 | } | ||
129 | case ALWAYS_FALSE -> vacuousQueries.add(canonicalQuery); | ||
130 | case ALWAYS_TRUE -> throw new IllegalArgumentException( | ||
131 | "Query %s is relationally unsafe (it matches every tuple)".formatted(query.name())); | ||
132 | default -> throw new IllegalArgumentException("Unknown reduction: " + reduction); | ||
133 | } | ||
134 | } | ||
135 | |||
136 | validateSymbols(store); | ||
137 | return new QueryInterpreterStoreAdapterImpl(store, buildEngineOptions(), dnf2PQuery.getSymbolViews(), | ||
138 | Collections.unmodifiableMap(canonicalQueryMap), Collections.unmodifiableMap(querySpecifications), | ||
139 | Collections.unmodifiableSet(vacuousQueries), store::checkCancelled); | ||
140 | } | ||
141 | |||
142 | private InterpreterEngineOptions buildEngineOptions() { | ||
143 | // Workaround: manually override the default backend, because {@link ViatraQueryEngineOptions.Builder} | ||
144 | // ignores all backend requirements except {@code SPECIFIC}. | ||
145 | switch (defaultHint.getQueryBackendRequirementType()) { | ||
146 | case SPECIFIC -> engineOptionsBuilder.withDefaultBackend(defaultHint.getQueryBackendFactory()); | ||
147 | case DEFAULT_CACHING -> engineOptionsBuilder.withDefaultBackend( | ||
148 | engineOptionsBuilder.build().getDefaultCachingBackendFactory()); | ||
149 | case DEFAULT_SEARCH -> engineOptionsBuilder.withDefaultBackend( | ||
150 | engineOptionsBuilder.build().getDefaultSearchBackendFactory()); | ||
151 | case UNSPECIFIED -> { | ||
152 | // Nothing to do, leave the default backend unchanged. | ||
153 | } | ||
154 | } | ||
155 | engineOptionsBuilder.withDefaultHint(defaultHint); | ||
156 | return engineOptionsBuilder.build(); | ||
157 | } | ||
158 | |||
159 | private void validateSymbols(ModelStore store) { | ||
160 | var symbols = store.getSymbols(); | ||
161 | for (var symbolView : dnf2PQuery.getSymbolViews().keySet()) { | ||
162 | var symbol = symbolView.getSymbol(); | ||
163 | if (!symbols.contains(symbol)) { | ||
164 | throw new IllegalArgumentException("Cannot query view %s: symbol %s is not in the model" | ||
165 | .formatted(symbolView, symbol)); | ||
166 | } | ||
167 | } | ||
168 | } | ||
169 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/QueryInterpreterStoreAdapterImpl.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/QueryInterpreterStoreAdapterImpl.java new file mode 100644 index 00000000..10e7a402 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/QueryInterpreterStoreAdapterImpl.java | |||
@@ -0,0 +1,100 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal; | ||
7 | |||
8 | import tools.refinery.interpreter.CancellationToken; | ||
9 | import tools.refinery.interpreter.api.IQuerySpecification; | ||
10 | import tools.refinery.interpreter.api.InterpreterEngineOptions; | ||
11 | import tools.refinery.interpreter.matchers.context.IInputKey; | ||
12 | import tools.refinery.store.model.Model; | ||
13 | import tools.refinery.store.model.ModelStore; | ||
14 | import tools.refinery.store.query.dnf.AnyQuery; | ||
15 | import tools.refinery.store.query.dnf.Query; | ||
16 | import tools.refinery.store.query.interpreter.QueryInterpreterStoreAdapter; | ||
17 | import tools.refinery.store.query.interpreter.internal.matcher.RawPatternMatcher; | ||
18 | import tools.refinery.store.query.view.AnySymbolView; | ||
19 | |||
20 | import java.util.*; | ||
21 | |||
22 | public class QueryInterpreterStoreAdapterImpl implements QueryInterpreterStoreAdapter { | ||
23 | private final ModelStore store; | ||
24 | private final InterpreterEngineOptions engineOptions; | ||
25 | private final Map<AnySymbolView, IInputKey> inputKeys; | ||
26 | private final Map<AnyQuery, AnyQuery> canonicalQueryMap; | ||
27 | private final Map<AnyQuery, IQuerySpecification<RawPatternMatcher>> querySpecifications; | ||
28 | private final Set<AnyQuery> vacuousQueries; | ||
29 | private final Set<AnyQuery> allQueries; | ||
30 | private final CancellationToken cancellationToken; | ||
31 | |||
32 | QueryInterpreterStoreAdapterImpl(ModelStore store, InterpreterEngineOptions engineOptions, | ||
33 | Map<AnySymbolView, IInputKey> inputKeys, | ||
34 | Map<AnyQuery, AnyQuery> canonicalQueryMap, | ||
35 | Map<AnyQuery, IQuerySpecification<RawPatternMatcher>> querySpecifications, | ||
36 | Set<AnyQuery> vacuousQueries, CancellationToken cancellationToken) { | ||
37 | this.store = store; | ||
38 | this.engineOptions = engineOptions; | ||
39 | this.inputKeys = inputKeys; | ||
40 | this.canonicalQueryMap = canonicalQueryMap; | ||
41 | this.querySpecifications = querySpecifications; | ||
42 | this.vacuousQueries = vacuousQueries; | ||
43 | this.cancellationToken = cancellationToken; | ||
44 | var mutableAllQueries = new LinkedHashSet<AnyQuery>(querySpecifications.size() + vacuousQueries.size()); | ||
45 | mutableAllQueries.addAll(querySpecifications.keySet()); | ||
46 | mutableAllQueries.addAll(vacuousQueries); | ||
47 | this.allQueries = Collections.unmodifiableSet(mutableAllQueries); | ||
48 | } | ||
49 | |||
50 | @Override | ||
51 | public ModelStore getStore() { | ||
52 | return store; | ||
53 | } | ||
54 | |||
55 | public Collection<AnySymbolView> getSymbolViews() { | ||
56 | return inputKeys.keySet(); | ||
57 | } | ||
58 | |||
59 | public Map<AnySymbolView, IInputKey> getInputKeys() { | ||
60 | return inputKeys; | ||
61 | } | ||
62 | |||
63 | @Override | ||
64 | public Collection<AnyQuery> getQueries() { | ||
65 | return allQueries; | ||
66 | } | ||
67 | |||
68 | public CancellationToken getCancellationToken() { | ||
69 | return cancellationToken; | ||
70 | } | ||
71 | |||
72 | @Override | ||
73 | public <T> Query<T> getCanonicalQuery(Query<T> query) { | ||
74 | // We know that canonical forms of queries do not change output types. | ||
75 | @SuppressWarnings("unchecked") | ||
76 | var canonicalQuery = (Query<T>) canonicalQueryMap.get(query); | ||
77 | if (canonicalQuery == null) { | ||
78 | throw new IllegalArgumentException("Unknown query: " + query); | ||
79 | } | ||
80 | return canonicalQuery; | ||
81 | } | ||
82 | |||
83 | Map<AnyQuery, IQuerySpecification<RawPatternMatcher>> getQuerySpecifications() { | ||
84 | return querySpecifications; | ||
85 | } | ||
86 | |||
87 | Set<AnyQuery> getVacuousQueries() { | ||
88 | return vacuousQueries; | ||
89 | } | ||
90 | |||
91 | @Override | ||
92 | public InterpreterEngineOptions getEngineOptions() { | ||
93 | return engineOptions; | ||
94 | } | ||
95 | |||
96 | @Override | ||
97 | public QueryInterpreterAdapterImpl createModelAdapter(Model model) { | ||
98 | return new QueryInterpreterAdapterImpl(model, this); | ||
99 | } | ||
100 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/RelationalScope.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/RelationalScope.java new file mode 100644 index 00000000..7eef5b85 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/RelationalScope.java | |||
@@ -0,0 +1,27 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal; | ||
7 | |||
8 | import org.apache.log4j.Logger; | ||
9 | import tools.refinery.interpreter.api.InterpreterEngine; | ||
10 | import tools.refinery.interpreter.api.scope.IEngineContext; | ||
11 | import tools.refinery.interpreter.api.scope.IIndexingErrorListener; | ||
12 | import tools.refinery.interpreter.api.scope.QueryScope; | ||
13 | import tools.refinery.store.query.interpreter.internal.context.RelationalEngineContext; | ||
14 | |||
15 | public class RelationalScope extends QueryScope { | ||
16 | private final QueryInterpreterAdapterImpl adapter; | ||
17 | |||
18 | public RelationalScope(QueryInterpreterAdapterImpl adapter) { | ||
19 | this.adapter = adapter; | ||
20 | } | ||
21 | |||
22 | @Override | ||
23 | protected IEngineContext createEngineContext(InterpreterEngine engine, IIndexingErrorListener errorListener, | ||
24 | Logger logger) { | ||
25 | return new RelationalEngineContext(adapter); | ||
26 | } | ||
27 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/context/DummyBaseIndexer.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/context/DummyBaseIndexer.java new file mode 100644 index 00000000..e9a05fe9 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/context/DummyBaseIndexer.java | |||
@@ -0,0 +1,66 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.context; | ||
7 | |||
8 | import tools.refinery.interpreter.api.scope.IBaseIndex; | ||
9 | import tools.refinery.interpreter.api.scope.IIndexingErrorListener; | ||
10 | import tools.refinery.interpreter.api.scope.IInstanceObserver; | ||
11 | import tools.refinery.interpreter.api.scope.InterpreterBaseIndexChangeListener; | ||
12 | |||
13 | import java.lang.reflect.InvocationTargetException; | ||
14 | import java.util.concurrent.Callable; | ||
15 | |||
16 | /** | ||
17 | * Copied from <code>tools.refinery.viatra.runtime.tabular.TabularEngineContext</code> | ||
18 | */ | ||
19 | public class DummyBaseIndexer implements IBaseIndex { | ||
20 | DummyBaseIndexer() { | ||
21 | } | ||
22 | |||
23 | @Override | ||
24 | public <V> V coalesceTraversals(Callable<V> callable) throws InvocationTargetException { | ||
25 | try { | ||
26 | return callable.call(); | ||
27 | } catch (Exception e) { | ||
28 | throw new InvocationTargetException(e); | ||
29 | } | ||
30 | } | ||
31 | |||
32 | @Override | ||
33 | public void addBaseIndexChangeListener(InterpreterBaseIndexChangeListener listener) { | ||
34 | // no notification support | ||
35 | } | ||
36 | |||
37 | @Override | ||
38 | public void removeBaseIndexChangeListener(InterpreterBaseIndexChangeListener listener) { | ||
39 | // no notification support | ||
40 | } | ||
41 | |||
42 | @Override | ||
43 | public void resampleDerivedFeatures() { | ||
44 | throw new UnsupportedOperationException(); | ||
45 | } | ||
46 | |||
47 | @Override | ||
48 | public boolean addIndexingErrorListener(IIndexingErrorListener listener) { | ||
49 | return false; | ||
50 | } | ||
51 | |||
52 | @Override | ||
53 | public boolean removeIndexingErrorListener(IIndexingErrorListener listener) { | ||
54 | return false; | ||
55 | } | ||
56 | |||
57 | @Override | ||
58 | public boolean addInstanceObserver(IInstanceObserver observer, Object observedObject) { | ||
59 | return false; | ||
60 | } | ||
61 | |||
62 | @Override | ||
63 | public boolean removeInstanceObserver(IInstanceObserver observer, Object observedObject) { | ||
64 | return false; | ||
65 | } | ||
66 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/context/RelationalEngineContext.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/context/RelationalEngineContext.java new file mode 100644 index 00000000..f6e8b605 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/context/RelationalEngineContext.java | |||
@@ -0,0 +1,35 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.context; | ||
7 | |||
8 | import tools.refinery.interpreter.api.scope.IBaseIndex; | ||
9 | import tools.refinery.interpreter.api.scope.IEngineContext; | ||
10 | import tools.refinery.interpreter.matchers.context.IQueryRuntimeContext; | ||
11 | import tools.refinery.store.query.interpreter.internal.QueryInterpreterAdapterImpl; | ||
12 | |||
13 | public class RelationalEngineContext implements IEngineContext { | ||
14 | private final IBaseIndex baseIndex = new DummyBaseIndexer(); | ||
15 | private final RelationalRuntimeContext runtimeContext; | ||
16 | |||
17 | public RelationalEngineContext(QueryInterpreterAdapterImpl adapter) { | ||
18 | runtimeContext = new RelationalRuntimeContext(adapter); | ||
19 | } | ||
20 | |||
21 | @Override | ||
22 | public IBaseIndex getBaseIndex() { | ||
23 | return this.baseIndex; | ||
24 | } | ||
25 | |||
26 | @Override | ||
27 | public void dispose() { | ||
28 | // Nothing to dispose, because lifecycle is not controlled by the engine. | ||
29 | } | ||
30 | |||
31 | @Override | ||
32 | public IQueryRuntimeContext getQueryRuntimeContext() { | ||
33 | return runtimeContext; | ||
34 | } | ||
35 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/context/RelationalQueryMetaContext.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/context/RelationalQueryMetaContext.java new file mode 100644 index 00000000..2b1ff2b4 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/context/RelationalQueryMetaContext.java | |||
@@ -0,0 +1,117 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.context; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.context.AbstractQueryMetaContext; | ||
9 | import tools.refinery.interpreter.matchers.context.IInputKey; | ||
10 | import tools.refinery.interpreter.matchers.context.InputKeyImplication; | ||
11 | import tools.refinery.interpreter.matchers.context.common.JavaTransitiveInstancesKey; | ||
12 | import tools.refinery.store.query.interpreter.internal.pquery.SymbolViewWrapper; | ||
13 | import tools.refinery.store.query.view.AnySymbolView; | ||
14 | |||
15 | import java.util.*; | ||
16 | |||
17 | /** | ||
18 | * The meta context information for String scopes. | ||
19 | */ | ||
20 | public class RelationalQueryMetaContext extends AbstractQueryMetaContext { | ||
21 | private final Map<AnySymbolView, IInputKey> inputKeys; | ||
22 | |||
23 | RelationalQueryMetaContext(Map<AnySymbolView, IInputKey> inputKeys) { | ||
24 | this.inputKeys = inputKeys; | ||
25 | } | ||
26 | |||
27 | @Override | ||
28 | public boolean isEnumerable(IInputKey key) { | ||
29 | checkKey(key); | ||
30 | return key.isEnumerable(); | ||
31 | } | ||
32 | |||
33 | @Override | ||
34 | public boolean isStateless(IInputKey key) { | ||
35 | checkKey(key); | ||
36 | return true; | ||
37 | } | ||
38 | |||
39 | @Override | ||
40 | public boolean canLeadOutOfScope(IInputKey key) { | ||
41 | return false; | ||
42 | } | ||
43 | |||
44 | @Override | ||
45 | public Collection<InputKeyImplication> getImplications(IInputKey implyingKey) { | ||
46 | if (implyingKey instanceof JavaTransitiveInstancesKey) { | ||
47 | return List.of(); | ||
48 | } | ||
49 | var symbolView = checkKey(implyingKey); | ||
50 | var relationViewImplications = symbolView.getImpliedRelationViews(); | ||
51 | var inputKeyImplications = new HashSet<InputKeyImplication>(relationViewImplications.size()); | ||
52 | for (var relationViewImplication : relationViewImplications) { | ||
53 | if (!symbolView.equals(relationViewImplication.implyingView())) { | ||
54 | throw new IllegalArgumentException("Relation view %s returned unrelated implication %s".formatted( | ||
55 | symbolView, relationViewImplication)); | ||
56 | } | ||
57 | var impliedInputKey = inputKeys.get(relationViewImplication.impliedView()); | ||
58 | // Ignore implications not relevant for any queries included in the model. | ||
59 | if (impliedInputKey != null) { | ||
60 | inputKeyImplications.add(new InputKeyImplication(implyingKey, impliedInputKey, | ||
61 | relationViewImplication.impliedIndices())); | ||
62 | } | ||
63 | } | ||
64 | var parameters = symbolView.getParameters(); | ||
65 | int arity = symbolView.arity(); | ||
66 | for (int i = 0; i < arity; i++) { | ||
67 | var parameter = parameters.get(i); | ||
68 | var parameterType = parameter.tryGetType(); | ||
69 | if (parameterType.isPresent()) { | ||
70 | var javaTransitiveInstancesKey = new JavaTransitiveInstancesKey(parameterType.get()); | ||
71 | var javaImplication = new InputKeyImplication(implyingKey, javaTransitiveInstancesKey, List.of(i)); | ||
72 | inputKeyImplications.add(javaImplication); | ||
73 | } | ||
74 | } | ||
75 | return inputKeyImplications; | ||
76 | } | ||
77 | |||
78 | @Override | ||
79 | public Map<Set<Integer>, Set<Integer>> getFunctionalDependencies(IInputKey key) { | ||
80 | if (key instanceof JavaTransitiveInstancesKey) { | ||
81 | return Map.of(); | ||
82 | } | ||
83 | var relationView = checkKey(key); | ||
84 | var functionalDependencies = relationView.getFunctionalDependencies(); | ||
85 | var flattened = new HashMap<Set<Integer>, Set<Integer>>(functionalDependencies.size()); | ||
86 | for (var functionalDependency : functionalDependencies) { | ||
87 | var forEach = functionalDependency.forEach(); | ||
88 | checkValidIndices(relationView, forEach); | ||
89 | var unique = functionalDependency.unique(); | ||
90 | checkValidIndices(relationView, unique); | ||
91 | var existing = flattened.get(forEach); | ||
92 | if (existing == null) { | ||
93 | flattened.put(forEach, new HashSet<>(unique)); | ||
94 | } else { | ||
95 | existing.addAll(unique); | ||
96 | } | ||
97 | } | ||
98 | return flattened; | ||
99 | } | ||
100 | |||
101 | private static void checkValidIndices(AnySymbolView relationView, Collection<Integer> indices) { | ||
102 | indices.stream().filter(relationView::invalidIndex).findAny().ifPresent(i -> { | ||
103 | throw new IllegalArgumentException("Index %d is invalid for %s".formatted(i, relationView)); | ||
104 | }); | ||
105 | } | ||
106 | |||
107 | public AnySymbolView checkKey(IInputKey key) { | ||
108 | if (!(key instanceof SymbolViewWrapper wrapper)) { | ||
109 | throw new IllegalArgumentException("The input key %s is not a valid input key".formatted(key)); | ||
110 | } | ||
111 | var symbolView = wrapper.getWrappedKey(); | ||
112 | if (!inputKeys.containsKey(symbolView)) { | ||
113 | throw new IllegalArgumentException("The input key %s is not present in the model".formatted(key)); | ||
114 | } | ||
115 | return symbolView; | ||
116 | } | ||
117 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/context/RelationalRuntimeContext.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/context/RelationalRuntimeContext.java new file mode 100644 index 00000000..5870b0c2 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/context/RelationalRuntimeContext.java | |||
@@ -0,0 +1,204 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.context; | ||
7 | |||
8 | import tools.refinery.interpreter.CancellationToken; | ||
9 | import tools.refinery.interpreter.matchers.context.*; | ||
10 | import tools.refinery.interpreter.matchers.tuple.ITuple; | ||
11 | import tools.refinery.interpreter.matchers.tuple.Tuple; | ||
12 | import tools.refinery.interpreter.matchers.tuple.TupleMask; | ||
13 | import tools.refinery.interpreter.matchers.tuple.Tuples; | ||
14 | import tools.refinery.interpreter.matchers.util.Accuracy; | ||
15 | import tools.refinery.store.model.Model; | ||
16 | import tools.refinery.store.query.interpreter.internal.QueryInterpreterAdapterImpl; | ||
17 | import tools.refinery.store.query.interpreter.internal.pquery.SymbolViewWrapper; | ||
18 | import tools.refinery.store.query.interpreter.internal.update.ModelUpdateListener; | ||
19 | import tools.refinery.store.query.view.AnySymbolView; | ||
20 | |||
21 | import java.lang.reflect.InvocationTargetException; | ||
22 | import java.util.Iterator; | ||
23 | import java.util.Optional; | ||
24 | import java.util.concurrent.Callable; | ||
25 | |||
26 | import static tools.refinery.store.util.CollectionsUtil.filter; | ||
27 | import static tools.refinery.store.util.CollectionsUtil.map; | ||
28 | |||
29 | public class RelationalRuntimeContext implements IQueryRuntimeContext { | ||
30 | private final RelationalQueryMetaContext metaContext; | ||
31 | |||
32 | private final ModelUpdateListener modelUpdateListener; | ||
33 | |||
34 | private final Model model; | ||
35 | |||
36 | private final CancellationToken cancellationToken; | ||
37 | |||
38 | RelationalRuntimeContext(QueryInterpreterAdapterImpl adapter) { | ||
39 | model = adapter.getModel(); | ||
40 | metaContext = new RelationalQueryMetaContext(adapter.getStoreAdapter().getInputKeys()); | ||
41 | modelUpdateListener = new ModelUpdateListener(adapter); | ||
42 | cancellationToken = adapter.getCancellationToken(); | ||
43 | } | ||
44 | |||
45 | @Override | ||
46 | public IQueryMetaContext getMetaContext() { | ||
47 | return metaContext; | ||
48 | } | ||
49 | |||
50 | @Override | ||
51 | public <V> V coalesceTraversals(Callable<V> callable) throws InvocationTargetException { | ||
52 | try { | ||
53 | return callable.call(); | ||
54 | } catch (Exception e) { | ||
55 | throw new InvocationTargetException(e); | ||
56 | } | ||
57 | } | ||
58 | |||
59 | @Override | ||
60 | public boolean isCoalescing() { | ||
61 | return false; | ||
62 | } | ||
63 | |||
64 | @Override | ||
65 | public boolean isIndexed(IInputKey key, IndexingService service) { | ||
66 | if (key instanceof SymbolViewWrapper wrapper) { | ||
67 | var symbolViewKey = wrapper.getWrappedKey(); | ||
68 | return this.modelUpdateListener.containsSymbolView(symbolViewKey); | ||
69 | } else { | ||
70 | return false; | ||
71 | } | ||
72 | } | ||
73 | |||
74 | @Override | ||
75 | public void ensureIndexed(IInputKey key, IndexingService service) { | ||
76 | if (!isIndexed(key, service)) { | ||
77 | throw new IllegalStateException("Engine tries to index a new key %s".formatted(key)); | ||
78 | } | ||
79 | } | ||
80 | |||
81 | AnySymbolView checkKey(IInputKey key) { | ||
82 | if (key instanceof SymbolViewWrapper wrappedKey) { | ||
83 | var symbolViewKey = wrappedKey.getWrappedKey(); | ||
84 | if (modelUpdateListener.containsSymbolView(symbolViewKey)) { | ||
85 | return symbolViewKey; | ||
86 | } else { | ||
87 | throw new IllegalStateException("Query is asking for non-indexed key %s".formatted(symbolViewKey)); | ||
88 | } | ||
89 | } else { | ||
90 | throw new IllegalStateException("Query is asking for non-relational key"); | ||
91 | } | ||
92 | } | ||
93 | |||
94 | @Override | ||
95 | public int countTuples(IInputKey key, TupleMask seedMask, ITuple seed) { | ||
96 | Iterator<Object[]> iterator = enumerate(key, seedMask, seed).iterator(); | ||
97 | int result = 0; | ||
98 | while (iterator.hasNext()) { | ||
99 | iterator.next(); | ||
100 | result++; | ||
101 | } | ||
102 | return result; | ||
103 | } | ||
104 | |||
105 | @Override | ||
106 | public Optional<Long> estimateCardinality(IInputKey key, TupleMask groupMask, Accuracy requiredAccuracy) { | ||
107 | return Optional.empty(); | ||
108 | } | ||
109 | |||
110 | @Override | ||
111 | public Iterable<Tuple> enumerateTuples(IInputKey key, TupleMask seedMask, ITuple seed) { | ||
112 | var filteredBySeed = enumerate(key, seedMask, seed); | ||
113 | return map(filteredBySeed, Tuples::flatTupleOf); | ||
114 | } | ||
115 | |||
116 | @Override | ||
117 | public Iterable<?> enumerateValues(IInputKey key, TupleMask seedMask, ITuple seed) { | ||
118 | var index = seedMask.getFirstOmittedIndex().orElseThrow( | ||
119 | () -> new IllegalArgumentException("Seed mask does not omit a value")); | ||
120 | var filteredBySeed = enumerate(key, seedMask, seed); | ||
121 | return map(filteredBySeed, array -> array[index]); | ||
122 | } | ||
123 | |||
124 | private Iterable<Object[]> enumerate(IInputKey key, TupleMask seedMask, ITuple seed) { | ||
125 | var relationViewKey = checkKey(key); | ||
126 | Iterable<Object[]> allObjects = getAllObjects(relationViewKey, seedMask, seed); | ||
127 | return filter(allObjects, objectArray -> isMatching(objectArray, seedMask, seed)); | ||
128 | } | ||
129 | |||
130 | private Iterable<Object[]> getAllObjects(AnySymbolView key, TupleMask seedMask, ITuple seed) { | ||
131 | for (int i = 0; i < seedMask.indices.length; i++) { | ||
132 | int slot = seedMask.indices[i]; | ||
133 | if (key.canIndexSlot(slot)) { | ||
134 | return key.getAdjacent(model, slot, seed.get(i)); | ||
135 | } | ||
136 | } | ||
137 | return key.getAll(model); | ||
138 | } | ||
139 | |||
140 | private static boolean isMatching(Object[] tuple, TupleMask seedMask, ITuple seed) { | ||
141 | for (int i = 0; i < seedMask.indices.length; i++) { | ||
142 | final Object seedElement = seed.get(i); | ||
143 | final Object tupleElement = tuple[seedMask.indices[i]]; | ||
144 | if (!tupleElement.equals(seedElement)) { | ||
145 | return false; | ||
146 | } | ||
147 | } | ||
148 | return true; | ||
149 | } | ||
150 | |||
151 | @Override | ||
152 | public boolean containsTuple(IInputKey key, ITuple seed) { | ||
153 | var relationViewKey = checkKey(key); | ||
154 | return relationViewKey.get(model, seed.getElements()); | ||
155 | } | ||
156 | |||
157 | @Override | ||
158 | public void addUpdateListener(IInputKey key, Tuple seed, IQueryRuntimeContextListener listener) { | ||
159 | var relationViewKey = checkKey(key); | ||
160 | this.modelUpdateListener.addListener(key, relationViewKey, seed, listener); | ||
161 | |||
162 | } | ||
163 | |||
164 | @Override | ||
165 | public void removeUpdateListener(IInputKey key, Tuple seed, IQueryRuntimeContextListener listener) { | ||
166 | var relationViewKey = checkKey(key); | ||
167 | this.modelUpdateListener.removeListener(key, relationViewKey, seed, listener); | ||
168 | } | ||
169 | |||
170 | @Override | ||
171 | public Object wrapElement(Object externalElement) { | ||
172 | return externalElement; | ||
173 | } | ||
174 | |||
175 | @Override | ||
176 | public Object unwrapElement(Object internalElement) { | ||
177 | return internalElement; | ||
178 | } | ||
179 | |||
180 | @Override | ||
181 | public Tuple wrapTuple(Tuple externalElements) { | ||
182 | return externalElements; | ||
183 | } | ||
184 | |||
185 | @Override | ||
186 | public Tuple unwrapTuple(Tuple internalElements) { | ||
187 | return internalElements; | ||
188 | } | ||
189 | |||
190 | @Override | ||
191 | public void ensureWildcardIndexing(IndexingService service) { | ||
192 | throw new UnsupportedOperationException(); | ||
193 | } | ||
194 | |||
195 | @Override | ||
196 | public void executeAfterTraversal(Runnable runnable) { | ||
197 | runnable.run(); | ||
198 | } | ||
199 | |||
200 | @Override | ||
201 | public CancellationToken getCancellationToken() { | ||
202 | return cancellationToken; | ||
203 | } | ||
204 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/localsearch/FlatCostFunction.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/localsearch/FlatCostFunction.java new file mode 100644 index 00000000..45fd9fbd --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/localsearch/FlatCostFunction.java | |||
@@ -0,0 +1,35 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.localsearch; | ||
7 | |||
8 | import tools.refinery.interpreter.localsearch.planner.cost.IConstraintEvaluationContext; | ||
9 | import tools.refinery.interpreter.localsearch.planner.cost.impl.StatisticsBasedConstraintCostFunction; | ||
10 | import tools.refinery.interpreter.matchers.context.IInputKey; | ||
11 | import tools.refinery.interpreter.matchers.psystem.basicenumerables.TypeConstraint; | ||
12 | import tools.refinery.interpreter.matchers.tuple.TupleMask; | ||
13 | import tools.refinery.interpreter.matchers.util.Accuracy; | ||
14 | |||
15 | import java.util.Optional; | ||
16 | |||
17 | public class FlatCostFunction extends StatisticsBasedConstraintCostFunction { | ||
18 | public FlatCostFunction() { | ||
19 | // No inverse navigation penalty thanks to relational storage. | ||
20 | super(0); | ||
21 | } | ||
22 | |||
23 | @Override | ||
24 | public Optional<Long> projectionSize(IConstraintEvaluationContext input, IInputKey supplierKey, TupleMask groupMask, Accuracy requiredAccuracy) { | ||
25 | // We always start from an empty model, where every projection is of size 0. | ||
26 | // Therefore, projection size estimation is meaningless. | ||
27 | return Optional.empty(); | ||
28 | } | ||
29 | |||
30 | @Override | ||
31 | protected double _calculateCost(TypeConstraint constraint, IConstraintEvaluationContext input) { | ||
32 | // Assume a flat cost for each relation. Maybe adjust in the future if we perform indexing? | ||
33 | return DEFAULT_COST; | ||
34 | } | ||
35 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/AbstractInterpretedMatcher.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/AbstractInterpretedMatcher.java new file mode 100644 index 00000000..8cec0bf6 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/AbstractInterpretedMatcher.java | |||
@@ -0,0 +1,32 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.matcher; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.backend.IQueryResultProvider; | ||
9 | import tools.refinery.interpreter.matchers.backend.IUpdateable; | ||
10 | import tools.refinery.store.query.dnf.Query; | ||
11 | import tools.refinery.store.query.resultset.AbstractResultSet; | ||
12 | import tools.refinery.store.query.interpreter.internal.QueryInterpreterAdapterImpl; | ||
13 | |||
14 | public abstract class AbstractInterpretedMatcher<T> extends AbstractResultSet<T> implements IUpdateable { | ||
15 | protected final IQueryResultProvider backend; | ||
16 | |||
17 | protected AbstractInterpretedMatcher(QueryInterpreterAdapterImpl adapter, Query<T> query, | ||
18 | RawPatternMatcher rawPatternMatcher) { | ||
19 | super(adapter, query); | ||
20 | backend = rawPatternMatcher.getBackend(); | ||
21 | } | ||
22 | |||
23 | @Override | ||
24 | protected void startListeningForChanges() { | ||
25 | backend.addUpdateListener(this, this, false); | ||
26 | } | ||
27 | |||
28 | @Override | ||
29 | protected void stopListeningForChanges() { | ||
30 | backend.removeUpdateListener(this); | ||
31 | } | ||
32 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/FunctionalCursor.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/FunctionalCursor.java new file mode 100644 index 00000000..e3b53f6b --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/FunctionalCursor.java | |||
@@ -0,0 +1,52 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.matcher; | ||
7 | |||
8 | import tools.refinery.interpreter.rete.index.IterableIndexer; | ||
9 | import tools.refinery.store.map.Cursor; | ||
10 | import tools.refinery.store.tuple.Tuple; | ||
11 | |||
12 | import java.util.Iterator; | ||
13 | |||
14 | class FunctionalCursor<T> implements Cursor<Tuple, T> { | ||
15 | private final IterableIndexer indexer; | ||
16 | private final Iterator<tools.refinery.interpreter.matchers.tuple.Tuple> iterator; | ||
17 | private boolean terminated; | ||
18 | private Tuple key; | ||
19 | private T value; | ||
20 | |||
21 | public FunctionalCursor(IterableIndexer indexer) { | ||
22 | this.indexer = indexer; | ||
23 | iterator = indexer.getSignatures().iterator(); | ||
24 | } | ||
25 | |||
26 | @Override | ||
27 | public Tuple getKey() { | ||
28 | return key; | ||
29 | } | ||
30 | |||
31 | @Override | ||
32 | public T getValue() { | ||
33 | return value; | ||
34 | } | ||
35 | |||
36 | @Override | ||
37 | public boolean isTerminated() { | ||
38 | return terminated; | ||
39 | } | ||
40 | |||
41 | @Override | ||
42 | public boolean move() { | ||
43 | if (!terminated && iterator.hasNext()) { | ||
44 | var match = iterator.next(); | ||
45 | key = MatcherUtils.toRefineryTuple(match); | ||
46 | value = MatcherUtils.getSingleValue(indexer.get(match)); | ||
47 | return true; | ||
48 | } | ||
49 | terminated = true; | ||
50 | return false; | ||
51 | } | ||
52 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/InterpretedFunctionalMatcher.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/InterpretedFunctionalMatcher.java new file mode 100644 index 00000000..249664a4 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/InterpretedFunctionalMatcher.java | |||
@@ -0,0 +1,89 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.matcher; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.context.IQueryRuntimeContext; | ||
9 | import tools.refinery.interpreter.matchers.tuple.TupleMask; | ||
10 | import tools.refinery.interpreter.matchers.tuple.Tuples; | ||
11 | import tools.refinery.interpreter.rete.index.IterableIndexer; | ||
12 | import tools.refinery.interpreter.rete.matcher.RetePatternMatcher; | ||
13 | import tools.refinery.store.map.Cursor; | ||
14 | import tools.refinery.store.query.dnf.FunctionalQuery; | ||
15 | import tools.refinery.store.query.interpreter.internal.QueryInterpreterAdapterImpl; | ||
16 | import tools.refinery.store.tuple.Tuple; | ||
17 | |||
18 | /** | ||
19 | * Directly access the tuples inside a Refinery Interpreter pattern matcher.<p> | ||
20 | * This class neglects calling | ||
21 | * {@link IQueryRuntimeContext#wrapTuple(tools.refinery.interpreter.matchers.tuple.Tuple)} | ||
22 | * and | ||
23 | * {@link IQueryRuntimeContext#unwrapTuple(tools.refinery.interpreter.matchers.tuple.Tuple)}, | ||
24 | * because {@link tools.refinery.store.query.interpreter.internal.context.RelationalRuntimeContext} provides a trivial | ||
25 | * implementation for these methods. | ||
26 | * Using this class with any other runtime context may lead to undefined behavior. | ||
27 | */ | ||
28 | public class InterpretedFunctionalMatcher<T> extends AbstractInterpretedMatcher<T> { | ||
29 | private final TupleMask emptyMask; | ||
30 | private final TupleMask omitOutputMask; | ||
31 | private final IterableIndexer omitOutputIndexer; | ||
32 | |||
33 | public InterpretedFunctionalMatcher(QueryInterpreterAdapterImpl adapter, FunctionalQuery<T> query, | ||
34 | RawPatternMatcher rawPatternMatcher) { | ||
35 | super(adapter, query, rawPatternMatcher); | ||
36 | int arity = query.arity(); | ||
37 | int arityWithOutput = arity + 1; | ||
38 | emptyMask = TupleMask.empty(arityWithOutput); | ||
39 | omitOutputMask = TupleMask.omit(arity, arityWithOutput); | ||
40 | if (backend instanceof RetePatternMatcher reteBackend) { | ||
41 | var maybeIterableOmitOutputIndexer = reteBackend.getInternalIndexer(omitOutputMask); | ||
42 | if (maybeIterableOmitOutputIndexer instanceof IterableIndexer iterableOmitOutputIndexer) { | ||
43 | omitOutputIndexer = iterableOmitOutputIndexer; | ||
44 | } else { | ||
45 | omitOutputIndexer = null; | ||
46 | } | ||
47 | } else { | ||
48 | omitOutputIndexer = null; | ||
49 | } | ||
50 | } | ||
51 | |||
52 | @Override | ||
53 | public T get(Tuple parameters) { | ||
54 | var tuple = MatcherUtils.toViatraTuple(parameters); | ||
55 | if (omitOutputIndexer == null) { | ||
56 | return MatcherUtils.getSingleValue(backend.getAllMatches(omitOutputMask, tuple).iterator()); | ||
57 | } else { | ||
58 | return MatcherUtils.getSingleValue(omitOutputIndexer.get(tuple)); | ||
59 | } | ||
60 | } | ||
61 | |||
62 | @Override | ||
63 | public Cursor<Tuple, T> getAll() { | ||
64 | if (omitOutputIndexer == null) { | ||
65 | var allMatches = backend.getAllMatches(emptyMask, Tuples.staticArityFlatTupleOf()); | ||
66 | return new UnsafeFunctionalCursor<>(allMatches.iterator()); | ||
67 | } | ||
68 | return new FunctionalCursor<>(omitOutputIndexer); | ||
69 | } | ||
70 | |||
71 | @Override | ||
72 | public int size() { | ||
73 | if (omitOutputIndexer == null) { | ||
74 | return backend.countMatches(emptyMask, Tuples.staticArityFlatTupleOf()); | ||
75 | } | ||
76 | return omitOutputIndexer.getBucketCount(); | ||
77 | } | ||
78 | |||
79 | @Override | ||
80 | public void update(tools.refinery.interpreter.matchers.tuple.Tuple updateElement, boolean isInsertion) { | ||
81 | var key = MatcherUtils.keyToRefineryTuple(updateElement); | ||
82 | var value = MatcherUtils.<T>getValue(updateElement); | ||
83 | if (isInsertion) { | ||
84 | notifyChange(key, null, value); | ||
85 | } else { | ||
86 | notifyChange(key, value, null); | ||
87 | } | ||
88 | } | ||
89 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/InterpretedRelationalMatcher.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/InterpretedRelationalMatcher.java new file mode 100644 index 00000000..9278b46d --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/InterpretedRelationalMatcher.java | |||
@@ -0,0 +1,81 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.matcher; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.context.IQueryRuntimeContext; | ||
9 | import tools.refinery.interpreter.matchers.tuple.TupleMask; | ||
10 | import tools.refinery.interpreter.matchers.tuple.Tuples; | ||
11 | import tools.refinery.interpreter.rete.index.Indexer; | ||
12 | import tools.refinery.interpreter.rete.matcher.RetePatternMatcher; | ||
13 | import tools.refinery.store.map.Cursor; | ||
14 | import tools.refinery.store.map.Cursors; | ||
15 | import tools.refinery.store.query.dnf.RelationalQuery; | ||
16 | import tools.refinery.store.query.interpreter.internal.QueryInterpreterAdapterImpl; | ||
17 | import tools.refinery.store.tuple.Tuple; | ||
18 | |||
19 | /** | ||
20 | * Directly access the tuples inside a Refinery Interpreter pattern matcher.<p> | ||
21 | * This class neglects calling | ||
22 | * {@link IQueryRuntimeContext#wrapTuple(tools.refinery.interpreter.matchers.tuple.Tuple)} | ||
23 | * and | ||
24 | * {@link IQueryRuntimeContext#unwrapTuple(tools.refinery.interpreter.matchers.tuple.Tuple)}, | ||
25 | * because {@link tools.refinery.store.query.interpreter.internal.context.RelationalRuntimeContext} provides a trivial | ||
26 | * implementation for these methods. | ||
27 | * Using this class with any other runtime context may lead to undefined behavior. | ||
28 | */ | ||
29 | public class InterpretedRelationalMatcher extends AbstractInterpretedMatcher<Boolean> { | ||
30 | private final TupleMask emptyMask; | ||
31 | private final TupleMask identityMask; | ||
32 | private final Indexer emptyMaskIndexer; | ||
33 | |||
34 | public InterpretedRelationalMatcher(QueryInterpreterAdapterImpl adapter, RelationalQuery query, | ||
35 | RawPatternMatcher rawPatternMatcher) { | ||
36 | super(adapter, query, rawPatternMatcher); | ||
37 | int arity = query.arity(); | ||
38 | emptyMask = TupleMask.empty(arity); | ||
39 | identityMask = TupleMask.identity(arity); | ||
40 | if (backend instanceof RetePatternMatcher reteBackend) { | ||
41 | emptyMaskIndexer = reteBackend.getInternalIndexer(emptyMask); | ||
42 | } else { | ||
43 | emptyMaskIndexer = null; | ||
44 | } | ||
45 | } | ||
46 | |||
47 | @Override | ||
48 | public Boolean get(Tuple parameters) { | ||
49 | var tuple = MatcherUtils.toViatraTuple(parameters); | ||
50 | if (emptyMaskIndexer == null) { | ||
51 | return backend.hasMatch(identityMask, tuple); | ||
52 | } | ||
53 | var matches = emptyMaskIndexer.get(Tuples.staticArityFlatTupleOf()); | ||
54 | return matches != null && matches.contains(tuple); | ||
55 | } | ||
56 | |||
57 | @Override | ||
58 | public Cursor<Tuple, Boolean> getAll() { | ||
59 | if (emptyMaskIndexer == null) { | ||
60 | var allMatches = backend.getAllMatches(emptyMask, Tuples.staticArityFlatTupleOf()); | ||
61 | return new RelationalCursor(allMatches.iterator()); | ||
62 | } | ||
63 | var matches = emptyMaskIndexer.get(Tuples.staticArityFlatTupleOf()); | ||
64 | return matches == null ? Cursors.empty() : new RelationalCursor(matches.stream().iterator()); | ||
65 | } | ||
66 | |||
67 | @Override | ||
68 | public int size() { | ||
69 | if (emptyMaskIndexer == null) { | ||
70 | return backend.countMatches(emptyMask, Tuples.staticArityFlatTupleOf()); | ||
71 | } | ||
72 | var matches = emptyMaskIndexer.get(Tuples.staticArityFlatTupleOf()); | ||
73 | return matches == null ? 0 : matches.size(); | ||
74 | } | ||
75 | |||
76 | @Override | ||
77 | public void update(tools.refinery.interpreter.matchers.tuple.Tuple updateElement, boolean isInsertion) { | ||
78 | var key = MatcherUtils.toRefineryTuple(updateElement); | ||
79 | notifyChange(key, !isInsertion, isInsertion); | ||
80 | } | ||
81 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/MatcherUtils.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/MatcherUtils.java new file mode 100644 index 00000000..b30b83b5 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/MatcherUtils.java | |||
@@ -0,0 +1,115 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.matcher; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.tuple.ITuple; | ||
9 | import tools.refinery.interpreter.matchers.tuple.Tuples; | ||
10 | import org.jetbrains.annotations.Nullable; | ||
11 | import tools.refinery.store.tuple.*; | ||
12 | |||
13 | import java.util.Iterator; | ||
14 | |||
15 | final class MatcherUtils { | ||
16 | private MatcherUtils() { | ||
17 | throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); | ||
18 | } | ||
19 | |||
20 | public static tools.refinery.interpreter.matchers.tuple.Tuple toViatraTuple(Tuple refineryTuple) { | ||
21 | if (refineryTuple instanceof Tuple0) { | ||
22 | return Tuples.staticArityFlatTupleOf(); | ||
23 | } else if (refineryTuple instanceof Tuple1) { | ||
24 | return Tuples.staticArityFlatTupleOf(refineryTuple); | ||
25 | } else if (refineryTuple instanceof Tuple2 tuple2) { | ||
26 | return Tuples.staticArityFlatTupleOf(Tuple.of(tuple2.value0()), Tuple.of(tuple2.value1())); | ||
27 | } else if (refineryTuple instanceof Tuple3 tuple3) { | ||
28 | return Tuples.staticArityFlatTupleOf(Tuple.of(tuple3.value0()), Tuple.of(tuple3.value1()), | ||
29 | Tuple.of(tuple3.value2())); | ||
30 | } else if (refineryTuple instanceof Tuple4 tuple4) { | ||
31 | return Tuples.staticArityFlatTupleOf(Tuple.of(tuple4.value0()), Tuple.of(tuple4.value1()), | ||
32 | Tuple.of(tuple4.value2()), Tuple.of(tuple4.value3())); | ||
33 | } else { | ||
34 | int arity = refineryTuple.getSize(); | ||
35 | var values = new Object[arity]; | ||
36 | for (int i = 0; i < arity; i++) { | ||
37 | values[i] = Tuple.of(refineryTuple.get(i)); | ||
38 | } | ||
39 | return Tuples.flatTupleOf(values); | ||
40 | } | ||
41 | } | ||
42 | |||
43 | public static Tuple toRefineryTuple(ITuple viatraTuple) { | ||
44 | int arity = viatraTuple.getSize(); | ||
45 | if (arity == 1) { | ||
46 | return getWrapper(viatraTuple, 0); | ||
47 | } | ||
48 | return prefixToRefineryTuple(viatraTuple, viatraTuple.getSize()); | ||
49 | } | ||
50 | |||
51 | public static Tuple keyToRefineryTuple(ITuple viatraTuple) { | ||
52 | return prefixToRefineryTuple(viatraTuple, viatraTuple.getSize() - 1); | ||
53 | } | ||
54 | |||
55 | private static Tuple prefixToRefineryTuple(ITuple viatraTuple, int targetArity) { | ||
56 | if (targetArity < 0) { | ||
57 | throw new IllegalArgumentException("Requested negative prefix %d of %s" | ||
58 | .formatted(targetArity, viatraTuple)); | ||
59 | } | ||
60 | return switch (targetArity) { | ||
61 | case 0 -> Tuple.of(); | ||
62 | case 1 -> Tuple.of(unwrap(viatraTuple, 0)); | ||
63 | case 2 -> Tuple.of(unwrap(viatraTuple, 0), unwrap(viatraTuple, 1)); | ||
64 | case 3 -> Tuple.of(unwrap(viatraTuple, 0), unwrap(viatraTuple, 1), unwrap(viatraTuple, 2)); | ||
65 | case 4 -> Tuple.of(unwrap(viatraTuple, 0), unwrap(viatraTuple, 1), unwrap(viatraTuple, 2), | ||
66 | unwrap(viatraTuple, 3)); | ||
67 | default -> { | ||
68 | var entries = new int[targetArity]; | ||
69 | for (int i = 0; i < targetArity; i++) { | ||
70 | entries[i] = unwrap(viatraTuple, i); | ||
71 | } | ||
72 | yield Tuple.of(entries); | ||
73 | } | ||
74 | }; | ||
75 | } | ||
76 | |||
77 | private static Tuple1 getWrapper(ITuple viatraTuple, int index) { | ||
78 | if (!((viatraTuple.get(index)) instanceof Tuple1 wrappedObjectId)) { | ||
79 | throw new IllegalArgumentException("Element %d of tuple %s is not an object id" | ||
80 | .formatted(index, viatraTuple)); | ||
81 | } | ||
82 | return wrappedObjectId; | ||
83 | } | ||
84 | |||
85 | private static int unwrap(ITuple viatraTuple, int index) { | ||
86 | return getWrapper(viatraTuple, index).value0(); | ||
87 | } | ||
88 | |||
89 | public static <T> T getValue(ITuple match) { | ||
90 | // This is only safe if we know for sure that match came from a functional query of type {@code T}. | ||
91 | @SuppressWarnings("unchecked") | ||
92 | var result = (T) match.get(match.getSize() - 1); | ||
93 | return result; | ||
94 | } | ||
95 | |||
96 | public static <T> T getSingleValue(@Nullable Iterable<? extends ITuple> viatraTuples) { | ||
97 | if (viatraTuples == null) { | ||
98 | return null; | ||
99 | } | ||
100 | return getSingleValue(viatraTuples.iterator()); | ||
101 | } | ||
102 | |||
103 | public static <T> T getSingleValue(Iterator<? extends ITuple> iterator) { | ||
104 | if (!iterator.hasNext()) { | ||
105 | return null; | ||
106 | } | ||
107 | var match = iterator.next(); | ||
108 | var result = MatcherUtils.<T>getValue(match); | ||
109 | if (iterator.hasNext()) { | ||
110 | var input = keyToRefineryTuple(match); | ||
111 | throw new IllegalStateException("Query is not functional for input tuple: " + input); | ||
112 | } | ||
113 | return result; | ||
114 | } | ||
115 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/RawPatternMatcher.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/RawPatternMatcher.java new file mode 100644 index 00000000..fcd5a236 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/RawPatternMatcher.java | |||
@@ -0,0 +1,20 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.matcher; | ||
7 | |||
8 | import tools.refinery.interpreter.api.GenericPatternMatcher; | ||
9 | import tools.refinery.interpreter.api.GenericQuerySpecification; | ||
10 | import tools.refinery.interpreter.matchers.backend.IQueryResultProvider; | ||
11 | |||
12 | public class RawPatternMatcher extends GenericPatternMatcher { | ||
13 | public RawPatternMatcher(GenericQuerySpecification<? extends GenericPatternMatcher> specification) { | ||
14 | super(specification); | ||
15 | } | ||
16 | |||
17 | IQueryResultProvider getBackend() { | ||
18 | return backend; | ||
19 | } | ||
20 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/RelationalCursor.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/RelationalCursor.java new file mode 100644 index 00000000..45eb9fff --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/RelationalCursor.java | |||
@@ -0,0 +1,47 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.matcher; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.tuple.ITuple; | ||
9 | import tools.refinery.store.map.Cursor; | ||
10 | import tools.refinery.store.tuple.Tuple; | ||
11 | |||
12 | import java.util.Iterator; | ||
13 | |||
14 | class RelationalCursor implements Cursor<Tuple, Boolean> { | ||
15 | private final Iterator<? extends ITuple> tuplesIterator; | ||
16 | private boolean terminated; | ||
17 | private Tuple key; | ||
18 | |||
19 | public RelationalCursor(Iterator<? extends ITuple> tuplesIterator) { | ||
20 | this.tuplesIterator = tuplesIterator; | ||
21 | } | ||
22 | |||
23 | @Override | ||
24 | public Tuple getKey() { | ||
25 | return key; | ||
26 | } | ||
27 | |||
28 | @Override | ||
29 | public Boolean getValue() { | ||
30 | return true; | ||
31 | } | ||
32 | |||
33 | @Override | ||
34 | public boolean isTerminated() { | ||
35 | return terminated; | ||
36 | } | ||
37 | |||
38 | @Override | ||
39 | public boolean move() { | ||
40 | if (!terminated && tuplesIterator.hasNext()) { | ||
41 | key = MatcherUtils.toRefineryTuple(tuplesIterator.next()); | ||
42 | return true; | ||
43 | } | ||
44 | terminated = true; | ||
45 | return false; | ||
46 | } | ||
47 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/UnsafeFunctionalCursor.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/UnsafeFunctionalCursor.java new file mode 100644 index 00000000..d3a7c743 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/matcher/UnsafeFunctionalCursor.java | |||
@@ -0,0 +1,55 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.matcher; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.tuple.ITuple; | ||
9 | import tools.refinery.store.map.Cursor; | ||
10 | import tools.refinery.store.tuple.Tuple; | ||
11 | |||
12 | import java.util.Iterator; | ||
13 | |||
14 | /** | ||
15 | * Cursor for a functional result set that iterates over a stream of raw matches and doesn't check whether the | ||
16 | * functional dependency of the output on the inputs is obeyed. | ||
17 | * @param <T> The output type. | ||
18 | */ | ||
19 | class UnsafeFunctionalCursor<T> implements Cursor<Tuple, T> { | ||
20 | private final Iterator<? extends ITuple> tuplesIterator; | ||
21 | private boolean terminated; | ||
22 | private Tuple key; | ||
23 | private T value; | ||
24 | |||
25 | public UnsafeFunctionalCursor(Iterator<? extends ITuple> tuplesIterator) { | ||
26 | this.tuplesIterator = tuplesIterator; | ||
27 | } | ||
28 | |||
29 | @Override | ||
30 | public Tuple getKey() { | ||
31 | return key; | ||
32 | } | ||
33 | |||
34 | @Override | ||
35 | public T getValue() { | ||
36 | return value; | ||
37 | } | ||
38 | |||
39 | @Override | ||
40 | public boolean isTerminated() { | ||
41 | return terminated; | ||
42 | } | ||
43 | |||
44 | @Override | ||
45 | public boolean move() { | ||
46 | if (!terminated && tuplesIterator.hasNext()) { | ||
47 | var match = tuplesIterator.next(); | ||
48 | key = MatcherUtils.keyToRefineryTuple(match); | ||
49 | value = MatcherUtils.getValue(match); | ||
50 | return true; | ||
51 | } | ||
52 | terminated = true; | ||
53 | return false; | ||
54 | } | ||
55 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/CheckEvaluator.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/CheckEvaluator.java new file mode 100644 index 00000000..4a71e879 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/CheckEvaluator.java | |||
@@ -0,0 +1,21 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.pquery; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.psystem.IValueProvider; | ||
9 | import tools.refinery.store.query.term.Term; | ||
10 | |||
11 | class CheckEvaluator extends TermEvaluator<Boolean> { | ||
12 | public CheckEvaluator(Term<Boolean> term) { | ||
13 | super(term); | ||
14 | } | ||
15 | |||
16 | @Override | ||
17 | public Object evaluateExpression(IValueProvider provider) { | ||
18 | var result = super.evaluateExpression(provider); | ||
19 | return result == null ? Boolean.FALSE : result; | ||
20 | } | ||
21 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/Dnf2PQuery.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/Dnf2PQuery.java new file mode 100644 index 00000000..73ce4043 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/Dnf2PQuery.java | |||
@@ -0,0 +1,253 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.pquery; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.psystem.basicdeferred.*; | ||
9 | import tools.refinery.interpreter.matchers.psystem.basicenumerables.*; | ||
10 | import tools.refinery.interpreter.matchers.psystem.basicenumerables.Connectivity; | ||
11 | import tools.refinery.store.query.Constraint; | ||
12 | import tools.refinery.store.query.dnf.Dnf; | ||
13 | import tools.refinery.store.query.dnf.DnfClause; | ||
14 | import tools.refinery.store.query.dnf.SymbolicParameter; | ||
15 | import tools.refinery.store.query.literal.*; | ||
16 | import tools.refinery.store.query.term.ConstantTerm; | ||
17 | import tools.refinery.store.query.term.StatefulAggregator; | ||
18 | import tools.refinery.store.query.term.StatelessAggregator; | ||
19 | import tools.refinery.store.query.term.Variable; | ||
20 | import tools.refinery.store.query.view.AnySymbolView; | ||
21 | import tools.refinery.store.util.CycleDetectingMapper; | ||
22 | import tools.refinery.interpreter.matchers.backend.IQueryBackendFactory; | ||
23 | import tools.refinery.interpreter.matchers.backend.QueryEvaluationHint; | ||
24 | import tools.refinery.interpreter.matchers.context.IInputKey; | ||
25 | import tools.refinery.interpreter.matchers.psystem.PBody; | ||
26 | import tools.refinery.interpreter.matchers.psystem.PVariable; | ||
27 | import tools.refinery.interpreter.matchers.psystem.aggregations.BoundAggregator; | ||
28 | import tools.refinery.interpreter.matchers.psystem.aggregations.IMultisetAggregationOperator; | ||
29 | import tools.refinery.interpreter.matchers.psystem.annotations.PAnnotation; | ||
30 | import tools.refinery.interpreter.matchers.psystem.queries.PParameter; | ||
31 | import tools.refinery.interpreter.matchers.psystem.queries.PParameterDirection; | ||
32 | import tools.refinery.interpreter.matchers.psystem.queries.PQuery; | ||
33 | import tools.refinery.interpreter.matchers.tuple.Tuple; | ||
34 | import tools.refinery.interpreter.matchers.tuple.Tuples; | ||
35 | |||
36 | import java.util.ArrayList; | ||
37 | import java.util.HashMap; | ||
38 | import java.util.List; | ||
39 | import java.util.Map; | ||
40 | import java.util.function.Function; | ||
41 | |||
42 | public class Dnf2PQuery { | ||
43 | private final CycleDetectingMapper<Dnf, RawPQuery> mapper = new CycleDetectingMapper<>(Dnf::name, | ||
44 | this::doTranslate); | ||
45 | private final QueryWrapperFactory wrapperFactory = new QueryWrapperFactory(this); | ||
46 | private Function<Dnf, QueryEvaluationHint> computeHint = dnf -> new QueryEvaluationHint(null, | ||
47 | (IQueryBackendFactory) null); | ||
48 | |||
49 | public void setComputeHint(Function<Dnf, QueryEvaluationHint> computeHint) { | ||
50 | this.computeHint = computeHint; | ||
51 | } | ||
52 | |||
53 | public RawPQuery translate(Dnf dnfQuery) { | ||
54 | return mapper.map(dnfQuery); | ||
55 | } | ||
56 | |||
57 | public Map<AnySymbolView, IInputKey> getSymbolViews() { | ||
58 | return wrapperFactory.getSymbolViews(); | ||
59 | } | ||
60 | |||
61 | private RawPQuery doTranslate(Dnf dnfQuery) { | ||
62 | var pQuery = new RawPQuery(dnfQuery.getUniqueName()); | ||
63 | pQuery.setEvaluationHints(computeHint.apply(dnfQuery)); | ||
64 | |||
65 | Map<SymbolicParameter, PParameter> parameters = new HashMap<>(); | ||
66 | List<PParameter> parameterList = new ArrayList<>(); | ||
67 | for (var parameter : dnfQuery.getSymbolicParameters()) { | ||
68 | var direction = switch (parameter.getDirection()) { | ||
69 | case OUT -> PParameterDirection.INOUT; | ||
70 | case IN -> throw new IllegalArgumentException("Query %s with input parameter %s is not supported" | ||
71 | .formatted(dnfQuery, parameter.getVariable())); | ||
72 | }; | ||
73 | var pParameter = new PParameter(parameter.getVariable().getUniqueName(), null, null, direction); | ||
74 | parameters.put(parameter, pParameter); | ||
75 | parameterList.add(pParameter); | ||
76 | } | ||
77 | |||
78 | pQuery.setParameters(parameterList); | ||
79 | |||
80 | for (var functionalDependency : dnfQuery.getFunctionalDependencies()) { | ||
81 | var functionalDependencyAnnotation = new PAnnotation("FunctionalDependency"); | ||
82 | for (var forEachVariable : functionalDependency.forEach()) { | ||
83 | functionalDependencyAnnotation.addAttribute("forEach", forEachVariable.getUniqueName()); | ||
84 | } | ||
85 | for (var uniqueVariable : functionalDependency.unique()) { | ||
86 | functionalDependencyAnnotation.addAttribute("unique", uniqueVariable.getUniqueName()); | ||
87 | } | ||
88 | pQuery.addAnnotation(functionalDependencyAnnotation); | ||
89 | } | ||
90 | |||
91 | for (DnfClause clause : dnfQuery.getClauses()) { | ||
92 | PBody body = new PBody(pQuery); | ||
93 | List<ExportedParameter> parameterExports = new ArrayList<>(); | ||
94 | for (var parameter : dnfQuery.getSymbolicParameters()) { | ||
95 | PVariable pVar = body.getOrCreateVariableByName(parameter.getVariable().getUniqueName()); | ||
96 | parameterExports.add(new ExportedParameter(body, pVar, parameters.get(parameter))); | ||
97 | } | ||
98 | body.setSymbolicParameters(parameterExports); | ||
99 | pQuery.addBody(body); | ||
100 | for (Literal literal : clause.literals()) { | ||
101 | translateLiteral(literal, body); | ||
102 | } | ||
103 | } | ||
104 | |||
105 | return pQuery; | ||
106 | } | ||
107 | |||
108 | private void translateLiteral(Literal literal, PBody body) { | ||
109 | if (literal instanceof EquivalenceLiteral equivalenceLiteral) { | ||
110 | translateEquivalenceLiteral(equivalenceLiteral, body); | ||
111 | } else if (literal instanceof CallLiteral callLiteral) { | ||
112 | translateCallLiteral(callLiteral, body); | ||
113 | } else if (literal instanceof ConstantLiteral constantLiteral) { | ||
114 | translateConstantLiteral(constantLiteral, body); | ||
115 | } else if (literal instanceof AssignLiteral<?> assignLiteral) { | ||
116 | translateAssignLiteral(assignLiteral, body); | ||
117 | } else if (literal instanceof CheckLiteral checkLiteral) { | ||
118 | translateCheckLiteral(checkLiteral, body); | ||
119 | } else if (literal instanceof CountLiteral countLiteral) { | ||
120 | translateCountLiteral(countLiteral, body); | ||
121 | } else if (literal instanceof AggregationLiteral<?, ?> aggregationLiteral) { | ||
122 | translateAggregationLiteral(aggregationLiteral, body); | ||
123 | } else if (literal instanceof RepresentativeElectionLiteral representativeElectionLiteral) { | ||
124 | translateRepresentativeElectionLiteral(representativeElectionLiteral, body); | ||
125 | } else { | ||
126 | throw new IllegalArgumentException("Unknown literal: " + literal.toString()); | ||
127 | } | ||
128 | } | ||
129 | |||
130 | private void translateEquivalenceLiteral(EquivalenceLiteral equivalenceLiteral, PBody body) { | ||
131 | PVariable varSource = body.getOrCreateVariableByName(equivalenceLiteral.getLeft().getUniqueName()); | ||
132 | PVariable varTarget = body.getOrCreateVariableByName(equivalenceLiteral.getRight().getUniqueName()); | ||
133 | if (equivalenceLiteral.isPositive()) { | ||
134 | new Equality(body, varSource, varTarget); | ||
135 | } else { | ||
136 | new Inequality(body, varSource, varTarget); | ||
137 | } | ||
138 | } | ||
139 | |||
140 | private void translateCallLiteral(CallLiteral callLiteral, PBody body) { | ||
141 | var polarity = callLiteral.getPolarity(); | ||
142 | switch (polarity) { | ||
143 | case POSITIVE -> { | ||
144 | var substitution = translateSubstitution(callLiteral.getArguments(), body); | ||
145 | var constraint = callLiteral.getTarget(); | ||
146 | if (constraint instanceof Dnf dnf) { | ||
147 | var pattern = translate(dnf); | ||
148 | new PositivePatternCall(body, substitution, pattern); | ||
149 | } else if (constraint instanceof AnySymbolView symbolView) { | ||
150 | var inputKey = wrapperFactory.getInputKey(symbolView); | ||
151 | new TypeConstraint(body, substitution, inputKey); | ||
152 | } else { | ||
153 | throw new IllegalArgumentException("Unknown Constraint: " + constraint); | ||
154 | } | ||
155 | } | ||
156 | case TRANSITIVE -> { | ||
157 | var substitution = translateSubstitution(callLiteral.getArguments(), body); | ||
158 | var pattern = wrapConstraintWithIdentityArguments(callLiteral.getTarget()); | ||
159 | new BinaryTransitiveClosure(body, substitution, pattern); | ||
160 | } | ||
161 | case NEGATIVE -> { | ||
162 | var wrappedCall = wrapperFactory.maybeWrapConstraint(callLiteral); | ||
163 | var substitution = translateSubstitution(wrappedCall.remappedArguments(), body); | ||
164 | var pattern = wrappedCall.pattern(); | ||
165 | new NegativePatternCall(body, substitution, pattern); | ||
166 | } | ||
167 | default -> throw new IllegalArgumentException("Unknown polarity: " + polarity); | ||
168 | } | ||
169 | } | ||
170 | |||
171 | private PQuery wrapConstraintWithIdentityArguments(Constraint constraint) { | ||
172 | if (constraint instanceof Dnf dnf) { | ||
173 | return translate(dnf); | ||
174 | } else if (constraint instanceof AnySymbolView symbolView) { | ||
175 | return wrapperFactory.wrapSymbolViewIdentityArguments(symbolView); | ||
176 | } else { | ||
177 | throw new IllegalArgumentException("Unknown Constraint: " + constraint); | ||
178 | } | ||
179 | } | ||
180 | |||
181 | private static Tuple translateSubstitution(List<Variable> substitution, PBody body) { | ||
182 | int arity = substitution.size(); | ||
183 | Object[] variables = new Object[arity]; | ||
184 | for (int i = 0; i < arity; i++) { | ||
185 | var variable = substitution.get(i); | ||
186 | variables[i] = body.getOrCreateVariableByName(variable.getUniqueName()); | ||
187 | } | ||
188 | return Tuples.flatTupleOf(variables); | ||
189 | } | ||
190 | |||
191 | private void translateConstantLiteral(ConstantLiteral constantLiteral, PBody body) { | ||
192 | var variable = body.getOrCreateVariableByName(constantLiteral.getVariable().getUniqueName()); | ||
193 | new ConstantValue(body, variable, tools.refinery.store.tuple.Tuple.of(constantLiteral.getNodeId())); | ||
194 | } | ||
195 | |||
196 | private <T> void translateAssignLiteral(AssignLiteral<T> assignLiteral, PBody body) { | ||
197 | var variable = body.getOrCreateVariableByName(assignLiteral.getVariable().getUniqueName()); | ||
198 | var term = assignLiteral.getTerm(); | ||
199 | if (term instanceof ConstantTerm<T> constantTerm) { | ||
200 | new ConstantValue(body, variable, constantTerm.getValue()); | ||
201 | } else { | ||
202 | var evaluator = new TermEvaluator<>(term); | ||
203 | new ExpressionEvaluation(body, evaluator, variable); | ||
204 | } | ||
205 | } | ||
206 | |||
207 | private void translateCheckLiteral(CheckLiteral checkLiteral, PBody body) { | ||
208 | var evaluator = new CheckEvaluator(checkLiteral.getTerm()); | ||
209 | new ExpressionEvaluation(body, evaluator, null); | ||
210 | } | ||
211 | |||
212 | private void translateCountLiteral(CountLiteral countLiteral, PBody body) { | ||
213 | var wrappedCall = wrapperFactory.maybeWrapConstraint(countLiteral); | ||
214 | var substitution = translateSubstitution(wrappedCall.remappedArguments(), body); | ||
215 | var resultVariable = body.getOrCreateVariableByName(countLiteral.getResultVariable().getUniqueName()); | ||
216 | new PatternMatchCounter(body, substitution, wrappedCall.pattern(), resultVariable); | ||
217 | } | ||
218 | |||
219 | private <R, T> void translateAggregationLiteral(AggregationLiteral<R, T> aggregationLiteral, PBody body) { | ||
220 | var aggregator = aggregationLiteral.getAggregator(); | ||
221 | IMultisetAggregationOperator<T, ?, R> aggregationOperator; | ||
222 | if (aggregator instanceof StatelessAggregator<R, T> statelessAggregator) { | ||
223 | aggregationOperator = new StatelessMultisetAggregator<>(statelessAggregator); | ||
224 | } else if (aggregator instanceof StatefulAggregator<R, T> statefulAggregator) { | ||
225 | aggregationOperator = new StatefulMultisetAggregator<>(statefulAggregator); | ||
226 | } else { | ||
227 | throw new IllegalArgumentException("Unknown aggregator: " + aggregator); | ||
228 | } | ||
229 | var wrappedCall = wrapperFactory.maybeWrapConstraint(aggregationLiteral); | ||
230 | var substitution = translateSubstitution(wrappedCall.remappedArguments(), body); | ||
231 | var inputVariable = body.getOrCreateVariableByName(aggregationLiteral.getInputVariable().getUniqueName()); | ||
232 | var aggregatedColumn = substitution.invertIndex().get(inputVariable); | ||
233 | if (aggregatedColumn == null) { | ||
234 | throw new IllegalStateException("Input variable %s not found in substitution %s".formatted(inputVariable, | ||
235 | substitution)); | ||
236 | } | ||
237 | var boundAggregator = new BoundAggregator(aggregationOperator, aggregator.getInputType(), | ||
238 | aggregator.getResultType()); | ||
239 | var resultVariable = body.getOrCreateVariableByName(aggregationLiteral.getResultVariable().getUniqueName()); | ||
240 | new AggregatorConstraint(boundAggregator, body, substitution, wrappedCall.pattern(), resultVariable, | ||
241 | aggregatedColumn); | ||
242 | } | ||
243 | |||
244 | private void translateRepresentativeElectionLiteral(RepresentativeElectionLiteral literal, PBody body) { | ||
245 | var substitution = translateSubstitution(literal.getArguments(), body); | ||
246 | var pattern = wrapConstraintWithIdentityArguments(literal.getTarget()); | ||
247 | var connectivity = switch (literal.getConnectivity()) { | ||
248 | case WEAK -> Connectivity.WEAK; | ||
249 | case STRONG -> Connectivity.STRONG; | ||
250 | }; | ||
251 | new RepresentativeElectionConstraint(body, substitution, pattern, connectivity); | ||
252 | } | ||
253 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/QueryWrapperFactory.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/QueryWrapperFactory.java new file mode 100644 index 00000000..a710dab3 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/QueryWrapperFactory.java | |||
@@ -0,0 +1,189 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.pquery; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.context.IInputKey; | ||
9 | import tools.refinery.interpreter.matchers.psystem.PBody; | ||
10 | import tools.refinery.interpreter.matchers.psystem.PVariable; | ||
11 | import tools.refinery.interpreter.matchers.psystem.basicdeferred.ExportedParameter; | ||
12 | import tools.refinery.interpreter.matchers.psystem.basicenumerables.PositivePatternCall; | ||
13 | import tools.refinery.interpreter.matchers.psystem.basicenumerables.TypeConstraint; | ||
14 | import tools.refinery.interpreter.matchers.psystem.queries.PParameter; | ||
15 | import tools.refinery.interpreter.matchers.psystem.queries.PQuery; | ||
16 | import tools.refinery.interpreter.matchers.psystem.queries.PVisibility; | ||
17 | import tools.refinery.interpreter.matchers.tuple.Tuple; | ||
18 | import tools.refinery.interpreter.matchers.tuple.Tuples; | ||
19 | import tools.refinery.store.query.Constraint; | ||
20 | import tools.refinery.store.query.dnf.Dnf; | ||
21 | import tools.refinery.store.query.dnf.DnfUtils; | ||
22 | import tools.refinery.store.query.literal.AbstractCallLiteral; | ||
23 | import tools.refinery.store.query.term.ParameterDirection; | ||
24 | import tools.refinery.store.query.term.Variable; | ||
25 | import tools.refinery.store.query.view.AnySymbolView; | ||
26 | import tools.refinery.store.query.view.SymbolView; | ||
27 | import tools.refinery.store.util.CycleDetectingMapper; | ||
28 | |||
29 | import java.util.*; | ||
30 | import java.util.function.ToIntFunction; | ||
31 | |||
32 | class QueryWrapperFactory { | ||
33 | private final Dnf2PQuery dnf2PQuery; | ||
34 | private final Map<AnySymbolView, SymbolViewWrapper> view2WrapperMap = new LinkedHashMap<>(); | ||
35 | private final CycleDetectingMapper<RemappedConstraint, RawPQuery> wrapConstraint = new CycleDetectingMapper<>( | ||
36 | this::doWrapConstraint); | ||
37 | |||
38 | QueryWrapperFactory(Dnf2PQuery dnf2PQuery) { | ||
39 | this.dnf2PQuery = dnf2PQuery; | ||
40 | } | ||
41 | |||
42 | public PQuery wrapSymbolViewIdentityArguments(AnySymbolView symbolView) { | ||
43 | var identity = new int[symbolView.arity()]; | ||
44 | for (int i = 0; i < identity.length; i++) { | ||
45 | identity[i] = i; | ||
46 | } | ||
47 | return maybeWrapConstraint(symbolView, identity); | ||
48 | } | ||
49 | |||
50 | public WrappedCall maybeWrapConstraint(AbstractCallLiteral callLiteral) { | ||
51 | var arguments = callLiteral.getArguments(); | ||
52 | int arity = arguments.size(); | ||
53 | var remappedParameters = new int[arity]; | ||
54 | var unboundVariableIndices = new HashMap<Variable, Integer>(); | ||
55 | var appendVariable = new VariableAppender(); | ||
56 | for (int i = 0; i < arity; i++) { | ||
57 | var variable = arguments.get(i); | ||
58 | // Unify all variables to avoid Refinery Interpreter bugs, even if they're bound in the containing clause. | ||
59 | remappedParameters[i] = unboundVariableIndices.computeIfAbsent(variable, appendVariable::applyAsInt); | ||
60 | } | ||
61 | var pattern = maybeWrapConstraint(callLiteral.getTarget(), remappedParameters); | ||
62 | return new WrappedCall(pattern, appendVariable.getRemappedArguments()); | ||
63 | } | ||
64 | |||
65 | private PQuery maybeWrapConstraint(Constraint constraint, int[] remappedParameters) { | ||
66 | if (remappedParameters.length != constraint.arity()) { | ||
67 | throw new IllegalArgumentException("Constraint %s expected %d parameters, but got %d parameters".formatted( | ||
68 | constraint, constraint.arity(), remappedParameters.length)); | ||
69 | } | ||
70 | if (constraint instanceof Dnf dnf && isIdentity(remappedParameters)) { | ||
71 | return dnf2PQuery.translate(dnf); | ||
72 | } | ||
73 | return wrapConstraint.map(new RemappedConstraint(constraint, remappedParameters)); | ||
74 | } | ||
75 | |||
76 | private static boolean isIdentity(int[] remappedParameters) { | ||
77 | for (int i = 0; i < remappedParameters.length; i++) { | ||
78 | if (remappedParameters[i] != i) { | ||
79 | return false; | ||
80 | } | ||
81 | } | ||
82 | return true; | ||
83 | } | ||
84 | |||
85 | private RawPQuery doWrapConstraint(RemappedConstraint remappedConstraint) { | ||
86 | var constraint = remappedConstraint.constraint(); | ||
87 | var remappedParameters = remappedConstraint.remappedParameters(); | ||
88 | |||
89 | checkNoInputParameters(constraint); | ||
90 | |||
91 | var embeddedPQuery = new RawPQuery(DnfUtils.generateUniqueName(constraint.name()), PVisibility.EMBEDDED); | ||
92 | var body = new PBody(embeddedPQuery); | ||
93 | int arity = Arrays.stream(remappedParameters).max().orElse(-1) + 1; | ||
94 | var parameters = new ArrayList<PParameter>(arity); | ||
95 | var parameterVariables = new PVariable[arity]; | ||
96 | var symbolicParameters = new ArrayList<ExportedParameter>(arity); | ||
97 | for (int i = 0; i < arity; i++) { | ||
98 | var parameterName = "p" + i; | ||
99 | var parameter = new PParameter(parameterName); | ||
100 | parameters.add(parameter); | ||
101 | var variable = body.getOrCreateVariableByName(parameterName); | ||
102 | parameterVariables[i] = variable; | ||
103 | symbolicParameters.add(new ExportedParameter(body, variable, parameter)); | ||
104 | } | ||
105 | embeddedPQuery.setParameters(parameters); | ||
106 | body.setSymbolicParameters(symbolicParameters); | ||
107 | |||
108 | var arguments = new Object[remappedParameters.length]; | ||
109 | for (int i = 0; i < remappedParameters.length; i++) { | ||
110 | arguments[i] = parameterVariables[remappedParameters[i]]; | ||
111 | } | ||
112 | var argumentTuple = Tuples.flatTupleOf(arguments); | ||
113 | |||
114 | addPositiveConstraint(constraint, body, argumentTuple); | ||
115 | embeddedPQuery.addBody(body); | ||
116 | return embeddedPQuery; | ||
117 | } | ||
118 | |||
119 | private static void checkNoInputParameters(Constraint constraint) { | ||
120 | for (var constraintParameter : constraint.getParameters()) { | ||
121 | if (constraintParameter.getDirection() == ParameterDirection.IN) { | ||
122 | throw new IllegalArgumentException("Input parameter %s of %s is not supported" | ||
123 | .formatted(constraintParameter, constraint)); | ||
124 | } | ||
125 | } | ||
126 | } | ||
127 | |||
128 | private void addPositiveConstraint(Constraint constraint, PBody body, Tuple argumentTuple) { | ||
129 | if (constraint instanceof SymbolView<?> view) { | ||
130 | new TypeConstraint(body, argumentTuple, getInputKey(view)); | ||
131 | } else if (constraint instanceof Dnf dnf) { | ||
132 | var calledPQuery = dnf2PQuery.translate(dnf); | ||
133 | new PositivePatternCall(body, argumentTuple, calledPQuery); | ||
134 | } else { | ||
135 | throw new IllegalArgumentException("Unknown Constraint: " + constraint); | ||
136 | } | ||
137 | } | ||
138 | |||
139 | public IInputKey getInputKey(AnySymbolView symbolView) { | ||
140 | return view2WrapperMap.computeIfAbsent(symbolView, SymbolViewWrapper::new); | ||
141 | } | ||
142 | |||
143 | public Map<AnySymbolView, IInputKey> getSymbolViews() { | ||
144 | return Collections.unmodifiableMap(view2WrapperMap); | ||
145 | } | ||
146 | |||
147 | public record WrappedCall(PQuery pattern, List<Variable> remappedArguments) { | ||
148 | } | ||
149 | |||
150 | private static class VariableAppender implements ToIntFunction<Variable> { | ||
151 | private final List<Variable> remappedArguments = new ArrayList<>(); | ||
152 | private int nextIndex = 0; | ||
153 | |||
154 | @Override | ||
155 | public int applyAsInt(Variable variable) { | ||
156 | remappedArguments.add(variable); | ||
157 | int index = nextIndex; | ||
158 | nextIndex++; | ||
159 | return index; | ||
160 | } | ||
161 | |||
162 | public List<Variable> getRemappedArguments() { | ||
163 | return remappedArguments; | ||
164 | } | ||
165 | } | ||
166 | |||
167 | private record RemappedConstraint(Constraint constraint, int[] remappedParameters) { | ||
168 | @Override | ||
169 | public boolean equals(Object o) { | ||
170 | if (this == o) return true; | ||
171 | if (o == null || getClass() != o.getClass()) return false; | ||
172 | RemappedConstraint that = (RemappedConstraint) o; | ||
173 | return constraint.equals(that.constraint) && Arrays.equals(remappedParameters, that.remappedParameters); | ||
174 | } | ||
175 | |||
176 | @Override | ||
177 | public int hashCode() { | ||
178 | int result = Objects.hash(constraint); | ||
179 | result = 31 * result + Arrays.hashCode(remappedParameters); | ||
180 | return result; | ||
181 | } | ||
182 | |||
183 | @Override | ||
184 | public String toString() { | ||
185 | return "RemappedConstraint{constraint=%s, remappedParameters=%s}".formatted(constraint, | ||
186 | Arrays.toString(remappedParameters)); | ||
187 | } | ||
188 | } | ||
189 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/RawPQuery.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/RawPQuery.java new file mode 100644 index 00000000..bbb35f91 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/RawPQuery.java | |||
@@ -0,0 +1,87 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.pquery; | ||
7 | |||
8 | import tools.refinery.interpreter.api.GenericQuerySpecification; | ||
9 | import tools.refinery.interpreter.api.InterpreterEngine; | ||
10 | import tools.refinery.interpreter.api.scope.QueryScope; | ||
11 | import tools.refinery.interpreter.matchers.psystem.PBody; | ||
12 | import tools.refinery.interpreter.matchers.psystem.annotations.PAnnotation; | ||
13 | import tools.refinery.interpreter.matchers.psystem.queries.BasePQuery; | ||
14 | import tools.refinery.interpreter.matchers.psystem.queries.PParameter; | ||
15 | import tools.refinery.interpreter.matchers.psystem.queries.PVisibility; | ||
16 | import tools.refinery.store.query.interpreter.internal.RelationalScope; | ||
17 | import tools.refinery.store.query.interpreter.internal.matcher.RawPatternMatcher; | ||
18 | |||
19 | import java.util.LinkedHashSet; | ||
20 | import java.util.List; | ||
21 | import java.util.Set; | ||
22 | |||
23 | public class RawPQuery extends BasePQuery { | ||
24 | private final String fullyQualifiedName; | ||
25 | private List<PParameter> parameters; | ||
26 | private final LinkedHashSet<PBody> bodies = new LinkedHashSet<>(); | ||
27 | |||
28 | public RawPQuery(String name, PVisibility visibility) { | ||
29 | super(visibility); | ||
30 | fullyQualifiedName = name; | ||
31 | } | ||
32 | |||
33 | public RawPQuery(String name) { | ||
34 | this(name, PVisibility.PUBLIC); | ||
35 | } | ||
36 | |||
37 | @Override | ||
38 | public String getFullyQualifiedName() { | ||
39 | return fullyQualifiedName; | ||
40 | } | ||
41 | |||
42 | public void setParameters(List<PParameter> parameters) { | ||
43 | this.parameters = parameters; | ||
44 | } | ||
45 | |||
46 | @Override | ||
47 | public void addAnnotation(PAnnotation annotation) { | ||
48 | super.addAnnotation(annotation); | ||
49 | } | ||
50 | |||
51 | @Override | ||
52 | public List<PParameter> getParameters() { | ||
53 | return parameters; | ||
54 | } | ||
55 | |||
56 | public void addBody(PBody body) { | ||
57 | bodies.add(body); | ||
58 | } | ||
59 | |||
60 | @Override | ||
61 | protected Set<PBody> doGetContainedBodies() { | ||
62 | return bodies; | ||
63 | } | ||
64 | |||
65 | public GenericQuerySpecification<RawPatternMatcher> build() { | ||
66 | return new GenericQuerySpecification<>(this) { | ||
67 | @Override | ||
68 | public Class<? extends QueryScope> getPreferredScopeClass() { | ||
69 | return RelationalScope.class; | ||
70 | } | ||
71 | |||
72 | @Override | ||
73 | protected RawPatternMatcher instantiate(InterpreterEngine engine) { | ||
74 | RawPatternMatcher matcher = engine.getExistingMatcher(this); | ||
75 | if (matcher == null) { | ||
76 | matcher = engine.getMatcher(this); | ||
77 | } | ||
78 | return matcher; | ||
79 | } | ||
80 | |||
81 | @Override | ||
82 | public RawPatternMatcher instantiate() { | ||
83 | return new RawPatternMatcher(this); | ||
84 | } | ||
85 | }; | ||
86 | } | ||
87 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/StatefulMultisetAggregator.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/StatefulMultisetAggregator.java new file mode 100644 index 00000000..7552117b --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/StatefulMultisetAggregator.java | |||
@@ -0,0 +1,65 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.pquery; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.psystem.aggregations.IMultisetAggregationOperator; | ||
9 | import tools.refinery.store.query.term.StatefulAggregate; | ||
10 | import tools.refinery.store.query.term.StatefulAggregator; | ||
11 | |||
12 | import java.util.stream.Stream; | ||
13 | |||
14 | record StatefulMultisetAggregator<R, T>(StatefulAggregator<R, T> aggregator) | ||
15 | implements IMultisetAggregationOperator<T, StatefulAggregate<R, T>, R> { | ||
16 | @Override | ||
17 | public String getShortDescription() { | ||
18 | return getName(); | ||
19 | } | ||
20 | |||
21 | @Override | ||
22 | public String getName() { | ||
23 | return aggregator.toString(); | ||
24 | } | ||
25 | |||
26 | @Override | ||
27 | public StatefulAggregate<R, T> createNeutral() { | ||
28 | return aggregator.createEmptyAggregate(); | ||
29 | } | ||
30 | |||
31 | @Override | ||
32 | public boolean isNeutral(StatefulAggregate<R, T> result) { | ||
33 | return result.isEmpty(); | ||
34 | } | ||
35 | |||
36 | @Override | ||
37 | public StatefulAggregate<R, T> update(StatefulAggregate<R, T> oldResult, T updateValue, boolean isInsertion) { | ||
38 | if (isInsertion) { | ||
39 | oldResult.add(updateValue); | ||
40 | } else { | ||
41 | oldResult.remove(updateValue); | ||
42 | } | ||
43 | return oldResult; | ||
44 | } | ||
45 | |||
46 | @Override | ||
47 | public R getAggregate(StatefulAggregate<R, T> result) { | ||
48 | return result.getResult(); | ||
49 | } | ||
50 | |||
51 | @Override | ||
52 | public R aggregateStream(Stream<T> stream) { | ||
53 | return aggregator.aggregateStream(stream); | ||
54 | } | ||
55 | |||
56 | @Override | ||
57 | public StatefulAggregate<R, T> clone(StatefulAggregate<R, T> original) { | ||
58 | return original.deepCopy(); | ||
59 | } | ||
60 | |||
61 | @Override | ||
62 | public boolean contains(T value, StatefulAggregate<R, T> accumulator) { | ||
63 | return accumulator.contains(value); | ||
64 | } | ||
65 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/StatelessMultisetAggregator.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/StatelessMultisetAggregator.java new file mode 100644 index 00000000..2da7ba87 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/StatelessMultisetAggregator.java | |||
@@ -0,0 +1,55 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.pquery; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.psystem.aggregations.IMultisetAggregationOperator; | ||
9 | import tools.refinery.store.query.term.StatelessAggregator; | ||
10 | |||
11 | import java.util.stream.Stream; | ||
12 | |||
13 | record StatelessMultisetAggregator<R, T>(StatelessAggregator<R, T> aggregator) | ||
14 | implements IMultisetAggregationOperator<T, R, R> { | ||
15 | @Override | ||
16 | public String getShortDescription() { | ||
17 | return getName(); | ||
18 | } | ||
19 | |||
20 | @Override | ||
21 | public String getName() { | ||
22 | return aggregator.toString(); | ||
23 | } | ||
24 | |||
25 | @Override | ||
26 | public R createNeutral() { | ||
27 | return aggregator.getEmptyResult(); | ||
28 | } | ||
29 | |||
30 | @Override | ||
31 | public boolean isNeutral(R result) { | ||
32 | return createNeutral().equals(result); | ||
33 | } | ||
34 | |||
35 | @Override | ||
36 | public R update(R oldResult, T updateValue, boolean isInsertion) { | ||
37 | return isInsertion ? aggregator.add(oldResult, updateValue) : aggregator.remove(oldResult, updateValue); | ||
38 | } | ||
39 | |||
40 | @Override | ||
41 | public R getAggregate(R result) { | ||
42 | return result; | ||
43 | } | ||
44 | |||
45 | @Override | ||
46 | public R clone(R original) { | ||
47 | // Aggregate result is immutable. | ||
48 | return original; | ||
49 | } | ||
50 | |||
51 | @Override | ||
52 | public R aggregateStream(Stream<T> stream) { | ||
53 | return aggregator.aggregateStream(stream); | ||
54 | } | ||
55 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/SymbolViewWrapper.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/SymbolViewWrapper.java new file mode 100644 index 00000000..51795f4a --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/SymbolViewWrapper.java | |||
@@ -0,0 +1,40 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.pquery; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.context.common.BaseInputKeyWrapper; | ||
9 | import tools.refinery.store.query.view.AnySymbolView; | ||
10 | |||
11 | public class SymbolViewWrapper extends BaseInputKeyWrapper<AnySymbolView> { | ||
12 | public SymbolViewWrapper(AnySymbolView wrappedKey) { | ||
13 | super(wrappedKey); | ||
14 | } | ||
15 | |||
16 | @Override | ||
17 | public String getPrettyPrintableName() { | ||
18 | return wrappedKey.name(); | ||
19 | } | ||
20 | |||
21 | @Override | ||
22 | public String getStringID() { | ||
23 | return getPrettyPrintableName(); | ||
24 | } | ||
25 | |||
26 | @Override | ||
27 | public int getArity() { | ||
28 | return wrappedKey.arity(); | ||
29 | } | ||
30 | |||
31 | @Override | ||
32 | public boolean isEnumerable() { | ||
33 | return true; | ||
34 | } | ||
35 | |||
36 | @Override | ||
37 | public String toString() { | ||
38 | return "RelationViewWrapper{wrappedKey=%s}".formatted(wrappedKey); | ||
39 | } | ||
40 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/TermEvaluator.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/TermEvaluator.java new file mode 100644 index 00000000..ed991091 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/TermEvaluator.java | |||
@@ -0,0 +1,37 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.pquery; | ||
7 | |||
8 | import tools.refinery.store.query.term.Term; | ||
9 | import tools.refinery.store.query.term.Variable; | ||
10 | import tools.refinery.interpreter.matchers.psystem.IExpressionEvaluator; | ||
11 | import tools.refinery.interpreter.matchers.psystem.IValueProvider; | ||
12 | |||
13 | import java.util.stream.Collectors; | ||
14 | |||
15 | class TermEvaluator<T> implements IExpressionEvaluator { | ||
16 | private final Term<T> term; | ||
17 | |||
18 | public TermEvaluator(Term<T> term) { | ||
19 | this.term = term; | ||
20 | } | ||
21 | |||
22 | @Override | ||
23 | public String getShortDescription() { | ||
24 | return term.toString(); | ||
25 | } | ||
26 | |||
27 | @Override | ||
28 | public Iterable<String> getInputParameterNames() { | ||
29 | return term.getInputVariables().stream().map(Variable::getUniqueName).collect(Collectors.toUnmodifiableSet()); | ||
30 | } | ||
31 | |||
32 | @Override | ||
33 | public Object evaluateExpression(IValueProvider provider) { | ||
34 | var valuation = new ValueProviderBasedValuation(provider); | ||
35 | return term.evaluate(valuation); | ||
36 | } | ||
37 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/ValueProviderBasedValuation.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/ValueProviderBasedValuation.java new file mode 100644 index 00000000..4124c9bb --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/pquery/ValueProviderBasedValuation.java | |||
@@ -0,0 +1,19 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.pquery; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.psystem.IValueProvider; | ||
9 | import tools.refinery.store.query.term.DataVariable; | ||
10 | import tools.refinery.store.query.valuation.Valuation; | ||
11 | |||
12 | public record ValueProviderBasedValuation(IValueProvider valueProvider) implements Valuation { | ||
13 | @Override | ||
14 | public <T> T getValue(DataVariable<T> variable) { | ||
15 | @SuppressWarnings("unchecked") | ||
16 | var value = (T) valueProvider.getValue(variable.getUniqueName()); | ||
17 | return value; | ||
18 | } | ||
19 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/ModelUpdateListener.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/ModelUpdateListener.java new file mode 100644 index 00000000..fad53675 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/ModelUpdateListener.java | |||
@@ -0,0 +1,51 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.update; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.context.IInputKey; | ||
9 | import tools.refinery.interpreter.matchers.context.IQueryRuntimeContextListener; | ||
10 | import tools.refinery.interpreter.matchers.tuple.ITuple; | ||
11 | import tools.refinery.store.query.interpreter.internal.QueryInterpreterAdapterImpl; | ||
12 | import tools.refinery.store.query.view.AnySymbolView; | ||
13 | import tools.refinery.store.query.view.SymbolView; | ||
14 | |||
15 | import java.util.HashMap; | ||
16 | import java.util.Map; | ||
17 | |||
18 | public class ModelUpdateListener { | ||
19 | private final Map<AnySymbolView, SymbolViewUpdateListener<?>> symbolViewUpdateListeners; | ||
20 | |||
21 | public ModelUpdateListener(QueryInterpreterAdapterImpl adapter) { | ||
22 | var symbolViews = adapter.getStoreAdapter().getInputKeys().keySet(); | ||
23 | symbolViewUpdateListeners = new HashMap<>(symbolViews.size()); | ||
24 | for (var symbolView : symbolViews) { | ||
25 | registerView(adapter, (SymbolView<?>) symbolView); | ||
26 | } | ||
27 | } | ||
28 | |||
29 | private <T> void registerView(QueryInterpreterAdapterImpl adapter, SymbolView<T> view) { | ||
30 | var model = adapter.getModel(); | ||
31 | var interpretation = model.getInterpretation(view.getSymbol()); | ||
32 | var listener = SymbolViewUpdateListener.of(adapter, view, interpretation); | ||
33 | symbolViewUpdateListeners.put(view, listener); | ||
34 | } | ||
35 | |||
36 | public boolean containsSymbolView(AnySymbolView relationView) { | ||
37 | return symbolViewUpdateListeners.containsKey(relationView); | ||
38 | } | ||
39 | |||
40 | public void addListener(IInputKey key, AnySymbolView symbolView, ITuple seed, | ||
41 | IQueryRuntimeContextListener listener) { | ||
42 | var symbolViewUpdateListener = symbolViewUpdateListeners.get(symbolView); | ||
43 | symbolViewUpdateListener.addFilter(key, seed, listener); | ||
44 | } | ||
45 | |||
46 | public void removeListener(IInputKey key, AnySymbolView symbolView, ITuple seed, | ||
47 | IQueryRuntimeContextListener listener) { | ||
48 | var symbolViewUpdateListener = symbolViewUpdateListeners.get(symbolView); | ||
49 | symbolViewUpdateListener.removeFilter(key, seed, listener); | ||
50 | } | ||
51 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/RelationViewFilter.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/RelationViewFilter.java new file mode 100644 index 00000000..4b4c73eb --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/RelationViewFilter.java | |||
@@ -0,0 +1,71 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.update; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.context.IInputKey; | ||
9 | import tools.refinery.interpreter.matchers.context.IQueryRuntimeContextListener; | ||
10 | import tools.refinery.interpreter.matchers.tuple.ITuple; | ||
11 | import tools.refinery.interpreter.matchers.tuple.Tuple; | ||
12 | |||
13 | import java.util.Arrays; | ||
14 | import java.util.Objects; | ||
15 | |||
16 | public final class RelationViewFilter { | ||
17 | private final IInputKey inputKey; | ||
18 | private final Object[] seed; | ||
19 | private final IQueryRuntimeContextListener listener; | ||
20 | |||
21 | public RelationViewFilter(IInputKey inputKey, ITuple seed, IQueryRuntimeContextListener listener) { | ||
22 | this.inputKey = inputKey; | ||
23 | this.seed = seedToArray(seed); | ||
24 | this.listener = listener; | ||
25 | } | ||
26 | |||
27 | public void update(Tuple updateTuple, boolean isInsertion) { | ||
28 | if (isMatching(updateTuple)) { | ||
29 | listener.update(inputKey, updateTuple, isInsertion); | ||
30 | } | ||
31 | } | ||
32 | |||
33 | private boolean isMatching(ITuple tuple) { | ||
34 | if (seed == null) { | ||
35 | return true; | ||
36 | } | ||
37 | int size = seed.length; | ||
38 | for (int i = 0; i < size; i++) { | ||
39 | var filterElement = seed[i]; | ||
40 | if (filterElement != null && !filterElement.equals(tuple.get(i))) { | ||
41 | return false; | ||
42 | } | ||
43 | } | ||
44 | return true; | ||
45 | } | ||
46 | |||
47 | // Use <code>null</code> instead of an empty array to speed up comparisons. | ||
48 | @SuppressWarnings("squid:S1168") | ||
49 | private static Object[] seedToArray(ITuple seed) { | ||
50 | for (var element : seed.getElements()) { | ||
51 | if (element != null) { | ||
52 | return seed.getElements(); | ||
53 | } | ||
54 | } | ||
55 | return null; | ||
56 | } | ||
57 | |||
58 | @Override | ||
59 | public boolean equals(Object obj) { | ||
60 | if (obj == this) return true; | ||
61 | if (obj == null || obj.getClass() != this.getClass()) return false; | ||
62 | var that = (RelationViewFilter) obj; | ||
63 | return Objects.equals(this.inputKey, that.inputKey) && Arrays.equals(this.seed, that.seed) && | ||
64 | Objects.equals(this.listener, that.listener); | ||
65 | } | ||
66 | |||
67 | @Override | ||
68 | public int hashCode() { | ||
69 | return Objects.hash(inputKey, Arrays.hashCode(seed), listener); | ||
70 | } | ||
71 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/SymbolViewUpdateListener.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/SymbolViewUpdateListener.java new file mode 100644 index 00000000..68020b11 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/SymbolViewUpdateListener.java | |||
@@ -0,0 +1,65 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.update; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.context.IInputKey; | ||
9 | import tools.refinery.interpreter.matchers.context.IQueryRuntimeContextListener; | ||
10 | import tools.refinery.interpreter.matchers.tuple.ITuple; | ||
11 | import tools.refinery.interpreter.matchers.tuple.Tuple; | ||
12 | import tools.refinery.store.model.Interpretation; | ||
13 | import tools.refinery.store.model.InterpretationListener; | ||
14 | import tools.refinery.store.query.interpreter.internal.QueryInterpreterAdapterImpl; | ||
15 | import tools.refinery.store.query.view.SymbolView; | ||
16 | import tools.refinery.store.query.view.TuplePreservingView; | ||
17 | |||
18 | import java.util.ArrayList; | ||
19 | import java.util.List; | ||
20 | |||
21 | public abstract class SymbolViewUpdateListener<T> implements InterpretationListener<T> { | ||
22 | private final QueryInterpreterAdapterImpl adapter; | ||
23 | private final Interpretation<T> interpretation; | ||
24 | private final List<RelationViewFilter> filters = new ArrayList<>(); | ||
25 | |||
26 | protected SymbolViewUpdateListener(QueryInterpreterAdapterImpl adapter, Interpretation<T> interpretation) { | ||
27 | this.adapter = adapter; | ||
28 | this.interpretation = interpretation; | ||
29 | } | ||
30 | |||
31 | public void addFilter(IInputKey inputKey, ITuple seed, IQueryRuntimeContextListener listener) { | ||
32 | if (filters.isEmpty()) { | ||
33 | // First filter to be added, from now on we have to subscribe to model updates. | ||
34 | interpretation.addListener(this, true); | ||
35 | } | ||
36 | filters.add(new RelationViewFilter(inputKey, seed, listener)); | ||
37 | } | ||
38 | |||
39 | public void removeFilter(IInputKey inputKey, ITuple seed, IQueryRuntimeContextListener listener) { | ||
40 | if (filters.remove(new RelationViewFilter(inputKey, seed, listener)) && filters.isEmpty()) { | ||
41 | // Last listener to be added, we don't have be subscribed to model updates anymore. | ||
42 | interpretation.removeListener(this); | ||
43 | } | ||
44 | } | ||
45 | |||
46 | protected void processUpdate(Tuple tuple, boolean isInsertion) { | ||
47 | adapter.markAsPending(); | ||
48 | int size = filters.size(); | ||
49 | // Use a for loop instead of a for-each loop to avoid <code>Iterator</code> allocation overhead. | ||
50 | //noinspection ForLoopReplaceableByForEach | ||
51 | for (int i = 0; i < size; i++) { | ||
52 | filters.get(i).update(tuple, isInsertion); | ||
53 | } | ||
54 | } | ||
55 | |||
56 | public static <T> SymbolViewUpdateListener<T> of(QueryInterpreterAdapterImpl adapter, | ||
57 | SymbolView<T> view, | ||
58 | Interpretation<T> interpretation) { | ||
59 | if (view instanceof TuplePreservingView<T> tuplePreservingRelationView) { | ||
60 | return new TuplePreservingViewUpdateListener<>(adapter, tuplePreservingRelationView, | ||
61 | interpretation); | ||
62 | } | ||
63 | return new TupleChangingViewUpdateListener<>(adapter, view, interpretation); | ||
64 | } | ||
65 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/TupleChangingViewUpdateListener.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/TupleChangingViewUpdateListener.java new file mode 100644 index 00000000..13b4af80 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/TupleChangingViewUpdateListener.java | |||
@@ -0,0 +1,45 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.update; | ||
7 | |||
8 | import tools.refinery.store.model.Interpretation; | ||
9 | import tools.refinery.store.query.interpreter.internal.QueryInterpreterAdapterImpl; | ||
10 | import tools.refinery.store.query.view.SymbolView; | ||
11 | import tools.refinery.store.tuple.Tuple; | ||
12 | import tools.refinery.interpreter.matchers.tuple.Tuples; | ||
13 | |||
14 | import java.util.Arrays; | ||
15 | |||
16 | public class TupleChangingViewUpdateListener<T> extends SymbolViewUpdateListener<T> { | ||
17 | private final SymbolView<T> view; | ||
18 | |||
19 | TupleChangingViewUpdateListener(QueryInterpreterAdapterImpl adapter, SymbolView<T> view, | ||
20 | Interpretation<T> interpretation) { | ||
21 | super(adapter, interpretation); | ||
22 | this.view = view; | ||
23 | } | ||
24 | |||
25 | @Override | ||
26 | public void put(Tuple key, T fromValue, T toValue, boolean restoring) { | ||
27 | boolean fromPresent = view.filter(key, fromValue); | ||
28 | boolean toPresent = view.filter(key, toValue); | ||
29 | if (fromPresent) { | ||
30 | var fromArray = view.forwardMap(key, fromValue); | ||
31 | if (toPresent) { // value change | ||
32 | var toArray = view.forwardMap(key, toValue); | ||
33 | if (!Arrays.equals(fromArray, toArray)) { | ||
34 | processUpdate(Tuples.flatTupleOf(fromArray), false); | ||
35 | processUpdate(Tuples.flatTupleOf(toArray), true); | ||
36 | } | ||
37 | } else { // fromValue disappears | ||
38 | processUpdate(Tuples.flatTupleOf(fromArray), false); | ||
39 | } | ||
40 | } else if (toPresent) { // toValue appears | ||
41 | var toArray = view.forwardMap(key, toValue); | ||
42 | processUpdate(Tuples.flatTupleOf(toArray), true); | ||
43 | } | ||
44 | } | ||
45 | } | ||
diff --git a/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/TuplePreservingViewUpdateListener.java b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/TuplePreservingViewUpdateListener.java new file mode 100644 index 00000000..c9f69145 --- /dev/null +++ b/subprojects/store-query-interpreter/src/main/java/tools/refinery/store/query/interpreter/internal/update/TuplePreservingViewUpdateListener.java | |||
@@ -0,0 +1,33 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.update; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.tuple.Tuples; | ||
9 | import tools.refinery.store.model.Interpretation; | ||
10 | import tools.refinery.store.query.interpreter.internal.QueryInterpreterAdapterImpl; | ||
11 | import tools.refinery.store.query.view.TuplePreservingView; | ||
12 | import tools.refinery.store.tuple.Tuple; | ||
13 | |||
14 | public class TuplePreservingViewUpdateListener<T> extends SymbolViewUpdateListener<T> { | ||
15 | private final TuplePreservingView<T> view; | ||
16 | |||
17 | TuplePreservingViewUpdateListener(QueryInterpreterAdapterImpl adapter, TuplePreservingView<T> view, | ||
18 | Interpretation<T> interpretation) { | ||
19 | super(adapter, interpretation); | ||
20 | this.view = view; | ||
21 | } | ||
22 | |||
23 | @Override | ||
24 | public void put(Tuple key, T fromValue, T toValue, boolean restoring) { | ||
25 | boolean fromPresent = view.filter(key, fromValue); | ||
26 | boolean toPresent = view.filter(key, toValue); | ||
27 | if (fromPresent == toPresent) { | ||
28 | return; | ||
29 | } | ||
30 | var translated = Tuples.flatTupleOf(view.forwardMap(key)); | ||
31 | processUpdate(translated, toPresent); | ||
32 | } | ||
33 | } | ||
diff --git a/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/DiagonalQueryTest.java b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/DiagonalQueryTest.java new file mode 100644 index 00000000..76de8679 --- /dev/null +++ b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/DiagonalQueryTest.java | |||
@@ -0,0 +1,391 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.backend.QueryEvaluationHint; | ||
9 | import tools.refinery.store.model.ModelStore; | ||
10 | import tools.refinery.store.query.ModelQueryAdapter; | ||
11 | import tools.refinery.store.query.dnf.Dnf; | ||
12 | import tools.refinery.store.query.dnf.Query; | ||
13 | import tools.refinery.store.query.interpreter.tests.QueryEngineTest; | ||
14 | import tools.refinery.store.query.view.AnySymbolView; | ||
15 | import tools.refinery.store.query.view.FunctionView; | ||
16 | import tools.refinery.store.query.view.KeyOnlyView; | ||
17 | import tools.refinery.store.representation.Symbol; | ||
18 | import tools.refinery.store.tuple.Tuple; | ||
19 | |||
20 | import java.util.List; | ||
21 | import java.util.Map; | ||
22 | import java.util.Optional; | ||
23 | |||
24 | import static tools.refinery.store.query.literal.Literals.not; | ||
25 | import static tools.refinery.store.query.term.int_.IntTerms.INT_SUM; | ||
26 | import static tools.refinery.store.query.interpreter.tests.QueryAssertions.assertNullableResults; | ||
27 | import static tools.refinery.store.query.interpreter.tests.QueryAssertions.assertResults; | ||
28 | |||
29 | class DiagonalQueryTest { | ||
30 | private static final Symbol<Boolean> person = Symbol.of("Person", 1); | ||
31 | private static final Symbol<Boolean> friend = Symbol.of("friend", 2); | ||
32 | private static final Symbol<Boolean> symbol = Symbol.of("symbol", 4); | ||
33 | private static final Symbol<Integer> intSymbol = Symbol.of("intSymbol", 4, Integer.class); | ||
34 | private static final AnySymbolView personView = new KeyOnlyView<>(person); | ||
35 | private static final AnySymbolView friendView = new KeyOnlyView<>(friend); | ||
36 | private static final AnySymbolView symbolView = new KeyOnlyView<>(symbol); | ||
37 | private static final FunctionView<Integer> intSymbolView = new FunctionView<>(intSymbol); | ||
38 | |||
39 | @QueryEngineTest | ||
40 | void inputKeyNegationTest(QueryEvaluationHint hint) { | ||
41 | var query = Query.of("Diagonal", (builder, p1) -> builder.clause(p2 -> List.of( | ||
42 | personView.call(p1), | ||
43 | not(symbolView.call(p1, p1, p2, p2)) | ||
44 | ))); | ||
45 | |||
46 | var store = ModelStore.builder() | ||
47 | .symbols(person, symbol) | ||
48 | .with(QueryInterpreterAdapter.builder() | ||
49 | .defaultHint(hint) | ||
50 | .queries(query)) | ||
51 | .build(); | ||
52 | |||
53 | var model = store.createEmptyModel(); | ||
54 | var personInterpretation = model.getInterpretation(person); | ||
55 | var symbolInterpretation = model.getInterpretation(symbol); | ||
56 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
57 | var queryResultSet = queryEngine.getResultSet(query); | ||
58 | |||
59 | personInterpretation.put(Tuple.of(0), true); | ||
60 | personInterpretation.put(Tuple.of(1), true); | ||
61 | personInterpretation.put(Tuple.of(2), true); | ||
62 | |||
63 | symbolInterpretation.put(Tuple.of(0, 0, 1, 1), true); | ||
64 | symbolInterpretation.put(Tuple.of(0, 0, 1, 2), true); | ||
65 | symbolInterpretation.put(Tuple.of(1, 1, 0, 1), true); | ||
66 | symbolInterpretation.put(Tuple.of(1, 2, 1, 1), true); | ||
67 | |||
68 | queryEngine.flushChanges(); | ||
69 | assertResults(Map.of( | ||
70 | Tuple.of(0), false, | ||
71 | Tuple.of(1), true, | ||
72 | Tuple.of(2), true, | ||
73 | Tuple.of(3), false | ||
74 | ), queryResultSet); | ||
75 | } | ||
76 | |||
77 | @QueryEngineTest | ||
78 | void subQueryNegationTest(QueryEvaluationHint hint) { | ||
79 | var subQuery = Query.of("SubQuery", (builder, p1, p2, p3, p4) -> builder | ||
80 | .clause( | ||
81 | personView.call(p1), | ||
82 | symbolView.call(p1, p2, p3, p4) | ||
83 | ) | ||
84 | .clause( | ||
85 | personView.call(p2), | ||
86 | symbolView.call(p1, p2, p3, p4) | ||
87 | )); | ||
88 | var query = Query.of("Diagonal", (builder, p1) -> builder.clause(p2 -> List.of( | ||
89 | personView.call(p1), | ||
90 | not(subQuery.call(p1, p1, p2, p2)) | ||
91 | ))); | ||
92 | |||
93 | var store = ModelStore.builder() | ||
94 | .symbols(person, symbol) | ||
95 | .with(QueryInterpreterAdapter.builder() | ||
96 | .defaultHint(hint) | ||
97 | .queries(query)) | ||
98 | .build(); | ||
99 | |||
100 | var model = store.createEmptyModel(); | ||
101 | |||
102 | var personInterpretation = model.getInterpretation(person); | ||
103 | var symbolInterpretation = model.getInterpretation(symbol); | ||
104 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
105 | var queryResultSet = queryEngine.getResultSet(query); | ||
106 | |||
107 | personInterpretation.put(Tuple.of(0), true); | ||
108 | personInterpretation.put(Tuple.of(1), true); | ||
109 | personInterpretation.put(Tuple.of(2), true); | ||
110 | |||
111 | symbolInterpretation.put(Tuple.of(0, 0, 1, 1), true); | ||
112 | symbolInterpretation.put(Tuple.of(0, 0, 1, 2), true); | ||
113 | symbolInterpretation.put(Tuple.of(1, 1, 0, 1), true); | ||
114 | symbolInterpretation.put(Tuple.of(1, 2, 1, 1), true); | ||
115 | |||
116 | queryEngine.flushChanges(); | ||
117 | assertResults(Map.of( | ||
118 | Tuple.of(0), false, | ||
119 | Tuple.of(1), true, | ||
120 | Tuple.of(2), true, | ||
121 | Tuple.of(3), false | ||
122 | ), queryResultSet); | ||
123 | } | ||
124 | |||
125 | @QueryEngineTest | ||
126 | void inputKeyCountTest(QueryEvaluationHint hint) { | ||
127 | var query = Query.of("Diagonal", Integer.class, (builder, p1, output) -> builder.clause(p2 -> List.of( | ||
128 | personView.call(p1), | ||
129 | output.assign(symbolView.count(p1, p1, p2, p2)) | ||
130 | ))); | ||
131 | |||
132 | var store = ModelStore.builder() | ||
133 | .symbols(person, symbol) | ||
134 | .with(QueryInterpreterAdapter.builder() | ||
135 | .defaultHint(hint) | ||
136 | .queries(query)) | ||
137 | .build(); | ||
138 | |||
139 | var model = store.createEmptyModel(); | ||
140 | var personInterpretation = model.getInterpretation(person); | ||
141 | var symbolInterpretation = model.getInterpretation(symbol); | ||
142 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
143 | var queryResultSet = queryEngine.getResultSet(query); | ||
144 | |||
145 | personInterpretation.put(Tuple.of(0), true); | ||
146 | personInterpretation.put(Tuple.of(1), true); | ||
147 | personInterpretation.put(Tuple.of(2), true); | ||
148 | |||
149 | symbolInterpretation.put(Tuple.of(0, 0, 1, 1), true); | ||
150 | symbolInterpretation.put(Tuple.of(0, 0, 2, 2), true); | ||
151 | symbolInterpretation.put(Tuple.of(0, 0, 1, 2), true); | ||
152 | symbolInterpretation.put(Tuple.of(1, 1, 0, 1), true); | ||
153 | symbolInterpretation.put(Tuple.of(1, 2, 1, 1), true); | ||
154 | |||
155 | queryEngine.flushChanges(); | ||
156 | assertNullableResults(Map.of( | ||
157 | Tuple.of(0), Optional.of(2), | ||
158 | Tuple.of(1), Optional.of(0), | ||
159 | Tuple.of(2), Optional.of(0), | ||
160 | Tuple.of(3), Optional.empty() | ||
161 | ), queryResultSet); | ||
162 | } | ||
163 | |||
164 | @QueryEngineTest | ||
165 | void subQueryCountTest(QueryEvaluationHint hint) { | ||
166 | var subQuery = Query.of("SubQuery", (builder, p1, p2, p3, p4) -> builder.clause( | ||
167 | personView.call(p1), | ||
168 | symbolView.call(p1, p2, p3, p4) | ||
169 | ) | ||
170 | .clause( | ||
171 | personView.call(p2), | ||
172 | symbolView.call(p1, p2, p3, p4) | ||
173 | )); | ||
174 | var query = Query.of("Diagonal", Integer.class, (builder, p1, output) -> builder.clause(p2 -> List.of( | ||
175 | personView.call(p1), | ||
176 | output.assign(subQuery.count(p1, p1, p2, p2)) | ||
177 | ))); | ||
178 | |||
179 | var store = ModelStore.builder() | ||
180 | .symbols(person, symbol) | ||
181 | .with(QueryInterpreterAdapter.builder() | ||
182 | .defaultHint(hint) | ||
183 | .queries(query)) | ||
184 | .build(); | ||
185 | |||
186 | var model = store.createEmptyModel(); | ||
187 | var personInterpretation = model.getInterpretation(person); | ||
188 | var symbolInterpretation = model.getInterpretation(symbol); | ||
189 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
190 | var queryResultSet = queryEngine.getResultSet(query); | ||
191 | |||
192 | personInterpretation.put(Tuple.of(0), true); | ||
193 | personInterpretation.put(Tuple.of(1), true); | ||
194 | personInterpretation.put(Tuple.of(2), true); | ||
195 | |||
196 | symbolInterpretation.put(Tuple.of(0, 0, 1, 1), true); | ||
197 | symbolInterpretation.put(Tuple.of(0, 0, 2, 2), true); | ||
198 | symbolInterpretation.put(Tuple.of(0, 0, 1, 2), true); | ||
199 | symbolInterpretation.put(Tuple.of(1, 1, 0, 1), true); | ||
200 | symbolInterpretation.put(Tuple.of(1, 2, 1, 1), true); | ||
201 | |||
202 | queryEngine.flushChanges(); | ||
203 | assertNullableResults(Map.of( | ||
204 | Tuple.of(0), Optional.of(2), | ||
205 | Tuple.of(1), Optional.of(0), | ||
206 | Tuple.of(2), Optional.of(0), | ||
207 | Tuple.of(3), Optional.empty() | ||
208 | ), queryResultSet); | ||
209 | } | ||
210 | |||
211 | @QueryEngineTest | ||
212 | void inputKeyAggregationTest(QueryEvaluationHint hint) { | ||
213 | var query = Query.of("Diagonal", Integer.class, (builder, p1, output) -> builder | ||
214 | .clause((p2) -> List.of( | ||
215 | personView.call(p1), | ||
216 | output.assign(intSymbolView.aggregate(INT_SUM, p1, p1, p2, p2)) | ||
217 | ))); | ||
218 | |||
219 | var store = ModelStore.builder() | ||
220 | .symbols(person, intSymbol) | ||
221 | .with(QueryInterpreterAdapter.builder() | ||
222 | .defaultHint(hint) | ||
223 | .queries(query)) | ||
224 | .build(); | ||
225 | |||
226 | var model = store.createEmptyModel(); | ||
227 | var personInterpretation = model.getInterpretation(person); | ||
228 | var intSymbolInterpretation = model.getInterpretation(intSymbol); | ||
229 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
230 | var queryResultSet = queryEngine.getResultSet(query); | ||
231 | |||
232 | personInterpretation.put(Tuple.of(0), true); | ||
233 | personInterpretation.put(Tuple.of(1), true); | ||
234 | personInterpretation.put(Tuple.of(2), true); | ||
235 | |||
236 | intSymbolInterpretation.put(Tuple.of(0, 0, 1, 1), 1); | ||
237 | intSymbolInterpretation.put(Tuple.of(0, 0, 2, 2), 2); | ||
238 | intSymbolInterpretation.put(Tuple.of(0, 0, 1, 2), 10); | ||
239 | intSymbolInterpretation.put(Tuple.of(1, 1, 0, 1), 11); | ||
240 | intSymbolInterpretation.put(Tuple.of(1, 2, 1, 1), 12); | ||
241 | |||
242 | queryEngine.flushChanges(); | ||
243 | assertNullableResults(Map.of( | ||
244 | Tuple.of(0), Optional.of(3), | ||
245 | Tuple.of(1), Optional.of(0), | ||
246 | Tuple.of(2), Optional.of(0), | ||
247 | Tuple.of(3), Optional.empty() | ||
248 | ), queryResultSet); | ||
249 | } | ||
250 | |||
251 | @QueryEngineTest | ||
252 | void subQueryAggregationTest(QueryEvaluationHint hint) { | ||
253 | var subQuery = Dnf.of("SubQuery", builder -> { | ||
254 | var p1 = builder.parameter("p1"); | ||
255 | var p2 = builder.parameter("p2"); | ||
256 | var p3 = builder.parameter("p3"); | ||
257 | var p4 = builder.parameter("p4"); | ||
258 | var x = builder.parameter("x", Integer.class); | ||
259 | var y = builder.parameter("y", Integer.class); | ||
260 | builder.clause( | ||
261 | personView.call(p1), | ||
262 | intSymbolView.call(p1, p2, p3, p4, x), | ||
263 | y.assign(x) | ||
264 | ); | ||
265 | builder.clause( | ||
266 | personView.call(p2), | ||
267 | intSymbolView.call(p1, p2, p3, p4, x), | ||
268 | y.assign(x) | ||
269 | ); | ||
270 | }); | ||
271 | var query = Query.of("Diagonal", Integer.class, (builder, p1, output) -> builder | ||
272 | .clause(Integer.class, Integer.class, (p2, y, z) -> List.of( | ||
273 | personView.call(p1), | ||
274 | output.assign(subQuery.aggregateBy(y, INT_SUM, p1, p1, p2, p2, y, z)) | ||
275 | ))); | ||
276 | |||
277 | var store = ModelStore.builder() | ||
278 | .symbols(person, intSymbol) | ||
279 | .with(QueryInterpreterAdapter.builder() | ||
280 | .defaultHint(hint) | ||
281 | .queries(query)) | ||
282 | .build(); | ||
283 | |||
284 | var model = store.createEmptyModel(); | ||
285 | var personInterpretation = model.getInterpretation(person); | ||
286 | var intSymbolInterpretation = model.getInterpretation(intSymbol); | ||
287 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
288 | var queryResultSet = queryEngine.getResultSet(query); | ||
289 | |||
290 | personInterpretation.put(Tuple.of(0), true); | ||
291 | personInterpretation.put(Tuple.of(1), true); | ||
292 | personInterpretation.put(Tuple.of(2), true); | ||
293 | |||
294 | intSymbolInterpretation.put(Tuple.of(0, 0, 1, 1), 1); | ||
295 | intSymbolInterpretation.put(Tuple.of(0, 0, 2, 2), 2); | ||
296 | intSymbolInterpretation.put(Tuple.of(0, 0, 1, 2), 10); | ||
297 | intSymbolInterpretation.put(Tuple.of(1, 1, 0, 1), 11); | ||
298 | intSymbolInterpretation.put(Tuple.of(1, 2, 1, 1), 12); | ||
299 | |||
300 | queryEngine.flushChanges(); | ||
301 | assertNullableResults(Map.of( | ||
302 | Tuple.of(0), Optional.of(3), | ||
303 | Tuple.of(1), Optional.of(0), | ||
304 | Tuple.of(2), Optional.of(0), | ||
305 | Tuple.of(3), Optional.empty() | ||
306 | ), queryResultSet); | ||
307 | } | ||
308 | |||
309 | @QueryEngineTest | ||
310 | void inputKeyTransitiveTest(QueryEvaluationHint hint) { | ||
311 | var query = Query.of("Diagonal", (builder, p1) -> builder.clause( | ||
312 | personView.call(p1), | ||
313 | friendView.callTransitive(p1, p1) | ||
314 | )); | ||
315 | |||
316 | var store = ModelStore.builder() | ||
317 | .symbols(person, friend) | ||
318 | .with(QueryInterpreterAdapter.builder() | ||
319 | .defaultHint(hint) | ||
320 | .queries(query)) | ||
321 | .build(); | ||
322 | |||
323 | var model = store.createEmptyModel(); | ||
324 | var personInterpretation = model.getInterpretation(person); | ||
325 | var friendInterpretation = model.getInterpretation(friend); | ||
326 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
327 | var queryResultSet = queryEngine.getResultSet(query); | ||
328 | |||
329 | personInterpretation.put(Tuple.of(0), true); | ||
330 | personInterpretation.put(Tuple.of(1), true); | ||
331 | personInterpretation.put(Tuple.of(2), true); | ||
332 | |||
333 | friendInterpretation.put(Tuple.of(0, 0), true); | ||
334 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
335 | friendInterpretation.put(Tuple.of(1, 2), true); | ||
336 | |||
337 | queryEngine.flushChanges(); | ||
338 | assertResults(Map.of( | ||
339 | Tuple.of(0), true, | ||
340 | Tuple.of(1), false, | ||
341 | Tuple.of(2), false, | ||
342 | Tuple.of(3), false | ||
343 | ), queryResultSet); | ||
344 | } | ||
345 | |||
346 | @QueryEngineTest | ||
347 | void subQueryTransitiveTest(QueryEvaluationHint hint) { | ||
348 | var subQuery = Query.of("SubQuery", (builder, p1, p2) -> builder | ||
349 | .clause( | ||
350 | personView.call(p1), | ||
351 | friendView.call(p1, p2) | ||
352 | ) | ||
353 | .clause( | ||
354 | personView.call(p2), | ||
355 | friendView.call(p1, p2) | ||
356 | )); | ||
357 | var query = Query.of("Diagonal", (builder, p1) -> builder.clause( | ||
358 | personView.call(p1), | ||
359 | subQuery.callTransitive(p1, p1) | ||
360 | )); | ||
361 | |||
362 | var store = ModelStore.builder() | ||
363 | .symbols(person, friend) | ||
364 | .with(QueryInterpreterAdapter.builder() | ||
365 | .defaultHint(hint) | ||
366 | .queries(query)) | ||
367 | .build(); | ||
368 | |||
369 | var model = store.createEmptyModel(); | ||
370 | var personInterpretation = model.getInterpretation(person); | ||
371 | var friendInterpretation = model.getInterpretation(friend); | ||
372 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
373 | var queryResultSet = queryEngine.getResultSet(query); | ||
374 | |||
375 | personInterpretation.put(Tuple.of(0), true); | ||
376 | personInterpretation.put(Tuple.of(1), true); | ||
377 | personInterpretation.put(Tuple.of(2), true); | ||
378 | |||
379 | friendInterpretation.put(Tuple.of(0, 0), true); | ||
380 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
381 | friendInterpretation.put(Tuple.of(1, 2), true); | ||
382 | |||
383 | queryEngine.flushChanges(); | ||
384 | assertResults(Map.of( | ||
385 | Tuple.of(0), true, | ||
386 | Tuple.of(1), false, | ||
387 | Tuple.of(2), false, | ||
388 | Tuple.of(3), false | ||
389 | ), queryResultSet); | ||
390 | } | ||
391 | } | ||
diff --git a/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/FunctionalQueryTest.java b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/FunctionalQueryTest.java new file mode 100644 index 00000000..00721a34 --- /dev/null +++ b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/FunctionalQueryTest.java | |||
@@ -0,0 +1,519 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.backend.QueryEvaluationHint; | ||
9 | import tools.refinery.store.map.Cursor; | ||
10 | import tools.refinery.store.model.ModelStore; | ||
11 | import tools.refinery.store.query.ModelQueryAdapter; | ||
12 | import tools.refinery.store.query.dnf.Query; | ||
13 | import tools.refinery.store.query.term.Variable; | ||
14 | import tools.refinery.store.query.interpreter.tests.QueryEngineTest; | ||
15 | import tools.refinery.store.query.view.AnySymbolView; | ||
16 | import tools.refinery.store.query.view.FilteredView; | ||
17 | import tools.refinery.store.query.view.FunctionView; | ||
18 | import tools.refinery.store.query.view.KeyOnlyView; | ||
19 | import tools.refinery.store.representation.Symbol; | ||
20 | import tools.refinery.store.representation.TruthValue; | ||
21 | import tools.refinery.store.tuple.Tuple; | ||
22 | |||
23 | import java.util.List; | ||
24 | import java.util.Map; | ||
25 | import java.util.Optional; | ||
26 | |||
27 | import static org.hamcrest.MatcherAssert.assertThat; | ||
28 | import static org.hamcrest.Matchers.is; | ||
29 | import static org.hamcrest.Matchers.nullValue; | ||
30 | import static org.junit.jupiter.api.Assertions.assertAll; | ||
31 | import static org.junit.jupiter.api.Assertions.assertThrows; | ||
32 | import static tools.refinery.store.query.literal.Literals.check; | ||
33 | import static tools.refinery.store.query.term.int_.IntTerms.*; | ||
34 | import static tools.refinery.store.query.interpreter.tests.QueryAssertions.assertNullableResults; | ||
35 | import static tools.refinery.store.query.interpreter.tests.QueryAssertions.assertResults; | ||
36 | |||
37 | class FunctionalQueryTest { | ||
38 | private static final Symbol<Boolean> person = Symbol.of("Person", 1); | ||
39 | private static final Symbol<Integer> age = Symbol.of("age", 1, Integer.class); | ||
40 | private static final Symbol<TruthValue> friend = Symbol.of("friend", 2, TruthValue.class, TruthValue.FALSE); | ||
41 | private static final AnySymbolView personView = new KeyOnlyView<>(person); | ||
42 | private static final FunctionView<Integer> ageView = new FunctionView<>(age); | ||
43 | private static final AnySymbolView friendMustView = new FilteredView<>(friend, "must", TruthValue::must); | ||
44 | |||
45 | @QueryEngineTest | ||
46 | void inputKeyTest(QueryEvaluationHint hint) { | ||
47 | var query = Query.of("InputKey", Integer.class, (builder, p1, output) -> builder.clause( | ||
48 | personView.call(p1), | ||
49 | ageView.call(p1, output) | ||
50 | )); | ||
51 | |||
52 | var store = ModelStore.builder() | ||
53 | .symbols(person, age) | ||
54 | .with(QueryInterpreterAdapter.builder() | ||
55 | .defaultHint(hint) | ||
56 | .queries(query)) | ||
57 | .build(); | ||
58 | |||
59 | var model = store.createEmptyModel(); | ||
60 | var personInterpretation = model.getInterpretation(person); | ||
61 | var ageInterpretation = model.getInterpretation(age); | ||
62 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
63 | var queryResultSet = queryEngine.getResultSet(query); | ||
64 | |||
65 | personInterpretation.put(Tuple.of(0), true); | ||
66 | personInterpretation.put(Tuple.of(1), true); | ||
67 | |||
68 | ageInterpretation.put(Tuple.of(0), 12); | ||
69 | ageInterpretation.put(Tuple.of(1), 24); | ||
70 | ageInterpretation.put(Tuple.of(2), 36); | ||
71 | |||
72 | queryEngine.flushChanges(); | ||
73 | assertNullableResults(Map.of( | ||
74 | Tuple.of(0), Optional.of(12), | ||
75 | Tuple.of(1), Optional.of(24), | ||
76 | Tuple.of(2), Optional.empty() | ||
77 | ), queryResultSet); | ||
78 | } | ||
79 | |||
80 | @QueryEngineTest | ||
81 | void predicateTest(QueryEvaluationHint hint) { | ||
82 | var subQuery = Query.of("SubQuery", Integer.class, (builder, p1, x) -> builder.clause( | ||
83 | personView.call(p1), | ||
84 | ageView.call(p1, x) | ||
85 | )); | ||
86 | var query = Query.of("Predicate", Integer.class, (builder, p1, output) -> builder.clause( | ||
87 | personView.call(p1), | ||
88 | output.assign(subQuery.call(p1)) | ||
89 | )); | ||
90 | |||
91 | var store = ModelStore.builder() | ||
92 | .symbols(person, age) | ||
93 | .with(QueryInterpreterAdapter.builder() | ||
94 | .defaultHint(hint) | ||
95 | .queries(query)) | ||
96 | .build(); | ||
97 | |||
98 | var model = store.createEmptyModel(); | ||
99 | var personInterpretation = model.getInterpretation(person); | ||
100 | var ageInterpretation = model.getInterpretation(age); | ||
101 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
102 | var queryResultSet = queryEngine.getResultSet(query); | ||
103 | |||
104 | personInterpretation.put(Tuple.of(0), true); | ||
105 | personInterpretation.put(Tuple.of(1), true); | ||
106 | |||
107 | ageInterpretation.put(Tuple.of(0), 12); | ||
108 | ageInterpretation.put(Tuple.of(1), 24); | ||
109 | ageInterpretation.put(Tuple.of(2), 36); | ||
110 | |||
111 | queryEngine.flushChanges(); | ||
112 | assertNullableResults(Map.of( | ||
113 | Tuple.of(0), Optional.of(12), | ||
114 | Tuple.of(1), Optional.of(24), | ||
115 | Tuple.of(2), Optional.empty() | ||
116 | ), queryResultSet); | ||
117 | } | ||
118 | |||
119 | @QueryEngineTest | ||
120 | void computationTest(QueryEvaluationHint hint) { | ||
121 | var query = Query.of("Computation", Integer.class, (builder, p1, output) -> builder.clause(() -> { | ||
122 | var x = Variable.of("x", Integer.class); | ||
123 | return List.of( | ||
124 | personView.call(p1), | ||
125 | ageView.call(p1, x), | ||
126 | output.assign(mul(x, constant(7))) | ||
127 | ); | ||
128 | })); | ||
129 | |||
130 | var store = ModelStore.builder() | ||
131 | .symbols(person, age) | ||
132 | .with(QueryInterpreterAdapter.builder() | ||
133 | .defaultHint(hint) | ||
134 | .queries(query)) | ||
135 | .build(); | ||
136 | |||
137 | var model = store.createEmptyModel(); | ||
138 | var personInterpretation = model.getInterpretation(person); | ||
139 | var ageInterpretation = model.getInterpretation(age); | ||
140 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
141 | var queryResultSet = queryEngine.getResultSet(query); | ||
142 | |||
143 | personInterpretation.put(Tuple.of(0), true); | ||
144 | personInterpretation.put(Tuple.of(1), true); | ||
145 | |||
146 | ageInterpretation.put(Tuple.of(0), 12); | ||
147 | ageInterpretation.put(Tuple.of(1), 24); | ||
148 | |||
149 | queryEngine.flushChanges(); | ||
150 | assertNullableResults(Map.of( | ||
151 | Tuple.of(0), Optional.of(84), | ||
152 | Tuple.of(1), Optional.of(168), | ||
153 | Tuple.of(2), Optional.empty() | ||
154 | ), queryResultSet); | ||
155 | } | ||
156 | |||
157 | @QueryEngineTest | ||
158 | void inputKeyCountTest(QueryEvaluationHint hint) { | ||
159 | var query = Query.of("Count", Integer.class, (builder, p1, output) -> builder.clause( | ||
160 | personView.call(p1), | ||
161 | output.assign(friendMustView.count(p1, Variable.of())) | ||
162 | )); | ||
163 | |||
164 | var store = ModelStore.builder() | ||
165 | .symbols(person, friend) | ||
166 | .with(QueryInterpreterAdapter.builder() | ||
167 | .defaultHint(hint) | ||
168 | .queries(query)) | ||
169 | .build(); | ||
170 | |||
171 | var model = store.createEmptyModel(); | ||
172 | var personInterpretation = model.getInterpretation(person); | ||
173 | var friendInterpretation = model.getInterpretation(friend); | ||
174 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
175 | var queryResultSet = queryEngine.getResultSet(query); | ||
176 | |||
177 | personInterpretation.put(Tuple.of(0), true); | ||
178 | personInterpretation.put(Tuple.of(1), true); | ||
179 | personInterpretation.put(Tuple.of(2), true); | ||
180 | |||
181 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
182 | friendInterpretation.put(Tuple.of(1, 0), TruthValue.TRUE); | ||
183 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
184 | |||
185 | queryEngine.flushChanges(); | ||
186 | assertNullableResults(Map.of( | ||
187 | Tuple.of(0), Optional.of(1), | ||
188 | Tuple.of(1), Optional.of(2), | ||
189 | Tuple.of(2), Optional.of(0), | ||
190 | Tuple.of(3), Optional.empty() | ||
191 | ), queryResultSet); | ||
192 | } | ||
193 | |||
194 | @QueryEngineTest | ||
195 | void predicateCountTest(QueryEvaluationHint hint) { | ||
196 | var subQuery = Query.of("SubQuery", (builder, p1, p2) -> builder.clause( | ||
197 | personView.call(p1), | ||
198 | personView.call(p2), | ||
199 | friendMustView.call(p1, p2) | ||
200 | )); | ||
201 | var query = Query.of("Count", Integer.class, (builder, p1, output) -> builder.clause( | ||
202 | personView.call(p1), | ||
203 | output.assign(subQuery.count(p1, Variable.of())) | ||
204 | )); | ||
205 | |||
206 | var store = ModelStore.builder() | ||
207 | .symbols(person, friend) | ||
208 | .with(QueryInterpreterAdapter.builder() | ||
209 | .defaultHint(hint) | ||
210 | .queries(query)) | ||
211 | .build(); | ||
212 | |||
213 | var model = store.createEmptyModel(); | ||
214 | var personInterpretation = model.getInterpretation(person); | ||
215 | var friendInterpretation = model.getInterpretation(friend); | ||
216 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
217 | var queryResultSet = queryEngine.getResultSet(query); | ||
218 | |||
219 | personInterpretation.put(Tuple.of(0), true); | ||
220 | personInterpretation.put(Tuple.of(1), true); | ||
221 | personInterpretation.put(Tuple.of(2), true); | ||
222 | |||
223 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
224 | friendInterpretation.put(Tuple.of(1, 0), TruthValue.TRUE); | ||
225 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
226 | |||
227 | queryEngine.flushChanges(); | ||
228 | assertNullableResults(Map.of( | ||
229 | Tuple.of(0), Optional.of(1), | ||
230 | Tuple.of(1), Optional.of(2), | ||
231 | Tuple.of(2), Optional.of(0), | ||
232 | Tuple.of(3), Optional.empty() | ||
233 | ), queryResultSet); | ||
234 | } | ||
235 | |||
236 | @QueryEngineTest | ||
237 | void inputKeyAggregationTest(QueryEvaluationHint hint) { | ||
238 | var query = Query.of("Aggregate", Integer.class, (builder, output) -> builder.clause( | ||
239 | output.assign(ageView.aggregate(INT_SUM, Variable.of())) | ||
240 | )); | ||
241 | |||
242 | var store = ModelStore.builder() | ||
243 | .symbols(age) | ||
244 | .with(QueryInterpreterAdapter.builder() | ||
245 | .defaultHint(hint) | ||
246 | .queries(query)) | ||
247 | .build(); | ||
248 | |||
249 | var model = store.createEmptyModel(); | ||
250 | var ageInterpretation = model.getInterpretation(age); | ||
251 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
252 | var queryResultSet = queryEngine.getResultSet(query); | ||
253 | |||
254 | ageInterpretation.put(Tuple.of(0), 12); | ||
255 | ageInterpretation.put(Tuple.of(1), 24); | ||
256 | |||
257 | queryEngine.flushChanges(); | ||
258 | assertResults(Map.of(Tuple.of(), 36), queryResultSet); | ||
259 | } | ||
260 | |||
261 | @QueryEngineTest | ||
262 | void predicateAggregationTest(QueryEvaluationHint hint) { | ||
263 | var subQuery = Query.of("SubQuery", Integer.class, (builder, p1, x) -> builder.clause( | ||
264 | personView.call(p1), | ||
265 | ageView.call(p1, x) | ||
266 | )); | ||
267 | var query = Query.of("Aggregate", Integer.class, (builder, output) -> builder.clause( | ||
268 | output.assign(subQuery.aggregate(INT_SUM, Variable.of())) | ||
269 | )); | ||
270 | |||
271 | var store = ModelStore.builder() | ||
272 | .symbols(person, age) | ||
273 | .with(QueryInterpreterAdapter.builder() | ||
274 | .defaultHint(hint) | ||
275 | .queries(query)) | ||
276 | .build(); | ||
277 | |||
278 | var model = store.createEmptyModel(); | ||
279 | var personInterpretation = model.getInterpretation(person); | ||
280 | var ageInterpretation = model.getInterpretation(age); | ||
281 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
282 | var queryResultSet = queryEngine.getResultSet(query); | ||
283 | |||
284 | personInterpretation.put(Tuple.of(0), true); | ||
285 | personInterpretation.put(Tuple.of(1), true); | ||
286 | |||
287 | ageInterpretation.put(Tuple.of(0), 12); | ||
288 | ageInterpretation.put(Tuple.of(1), 24); | ||
289 | |||
290 | queryEngine.flushChanges(); | ||
291 | assertResults(Map.of(Tuple.of(), 36), queryResultSet); | ||
292 | } | ||
293 | |||
294 | @QueryEngineTest | ||
295 | void extremeValueTest(QueryEvaluationHint hint) { | ||
296 | var subQuery = Query.of("SubQuery", Integer.class, (builder, p1, x) -> builder.clause( | ||
297 | personView.call(p1), | ||
298 | x.assign(friendMustView.count(p1, Variable.of())) | ||
299 | )); | ||
300 | var minQuery = Query.of("Min", Integer.class, (builder, output) -> builder.clause( | ||
301 | output.assign(subQuery.aggregate(INT_MIN, Variable.of())) | ||
302 | )); | ||
303 | var maxQuery = Query.of("Max", Integer.class, (builder, output) -> builder.clause( | ||
304 | output.assign(subQuery.aggregate(INT_MAX, Variable.of())) | ||
305 | )); | ||
306 | |||
307 | var store = ModelStore.builder() | ||
308 | .symbols(person, friend) | ||
309 | .with(QueryInterpreterAdapter.builder() | ||
310 | .defaultHint(hint) | ||
311 | .queries(minQuery, maxQuery)) | ||
312 | .build(); | ||
313 | |||
314 | var model = store.createEmptyModel(); | ||
315 | var personInterpretation = model.getInterpretation(person); | ||
316 | var friendInterpretation = model.getInterpretation(friend); | ||
317 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
318 | var minResultSet = queryEngine.getResultSet(minQuery); | ||
319 | var maxResultSet = queryEngine.getResultSet(maxQuery); | ||
320 | |||
321 | assertResults(Map.of(Tuple.of(), Integer.MAX_VALUE), minResultSet); | ||
322 | assertResults(Map.of(Tuple.of(), Integer.MIN_VALUE), maxResultSet); | ||
323 | |||
324 | personInterpretation.put(Tuple.of(0), true); | ||
325 | personInterpretation.put(Tuple.of(1), true); | ||
326 | personInterpretation.put(Tuple.of(2), true); | ||
327 | |||
328 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
329 | friendInterpretation.put(Tuple.of(1, 0), TruthValue.TRUE); | ||
330 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
331 | |||
332 | queryEngine.flushChanges(); | ||
333 | assertResults(Map.of(Tuple.of(), 0), minResultSet); | ||
334 | assertResults(Map.of(Tuple.of(), 2), maxResultSet); | ||
335 | |||
336 | friendInterpretation.put(Tuple.of(2, 0), TruthValue.TRUE); | ||
337 | friendInterpretation.put(Tuple.of(2, 1), TruthValue.TRUE); | ||
338 | |||
339 | queryEngine.flushChanges(); | ||
340 | assertResults(Map.of(Tuple.of(), 1), minResultSet); | ||
341 | assertResults(Map.of(Tuple.of(), 2), maxResultSet); | ||
342 | |||
343 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.FALSE); | ||
344 | friendInterpretation.put(Tuple.of(1, 0), TruthValue.FALSE); | ||
345 | friendInterpretation.put(Tuple.of(2, 0), TruthValue.FALSE); | ||
346 | |||
347 | queryEngine.flushChanges(); | ||
348 | assertResults(Map.of(Tuple.of(), 0), minResultSet); | ||
349 | assertResults(Map.of(Tuple.of(), 1), maxResultSet); | ||
350 | } | ||
351 | |||
352 | @QueryEngineTest | ||
353 | void invalidComputationTest(QueryEvaluationHint hint) { | ||
354 | var query = Query.of("InvalidComputation", Integer.class, | ||
355 | (builder, p1, output) -> builder.clause(Integer.class, (x) -> List.of( | ||
356 | personView.call(p1), | ||
357 | ageView.call(p1, x), | ||
358 | output.assign(div(constant(120), x)) | ||
359 | ))); | ||
360 | |||
361 | var store = ModelStore.builder() | ||
362 | .symbols(person, age) | ||
363 | .with(QueryInterpreterAdapter.builder() | ||
364 | .defaultHint(hint) | ||
365 | .queries(query)) | ||
366 | .build(); | ||
367 | |||
368 | var model = store.createEmptyModel(); | ||
369 | var personInterpretation = model.getInterpretation(person); | ||
370 | var ageInterpretation = model.getInterpretation(age); | ||
371 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
372 | var queryResultSet = queryEngine.getResultSet(query); | ||
373 | |||
374 | personInterpretation.put(Tuple.of(0), true); | ||
375 | personInterpretation.put(Tuple.of(1), true); | ||
376 | |||
377 | ageInterpretation.put(Tuple.of(0), 0); | ||
378 | ageInterpretation.put(Tuple.of(1), 30); | ||
379 | |||
380 | queryEngine.flushChanges(); | ||
381 | assertNullableResults(Map.of( | ||
382 | Tuple.of(0), Optional.empty(), | ||
383 | Tuple.of(1), Optional.of(4), | ||
384 | Tuple.of(2), Optional.empty() | ||
385 | ), queryResultSet); | ||
386 | } | ||
387 | |||
388 | @QueryEngineTest | ||
389 | void invalidAssumeTest(QueryEvaluationHint hint) { | ||
390 | var query = Query.of("InvalidAssume", (builder, p1) -> builder.clause(Integer.class, (x) -> List.of( | ||
391 | personView.call(p1), | ||
392 | ageView.call(p1, x), | ||
393 | check(lessEq(div(constant(120), x), constant(5))) | ||
394 | ))); | ||
395 | |||
396 | var store = ModelStore.builder() | ||
397 | .symbols(person, age) | ||
398 | .with(QueryInterpreterAdapter.builder() | ||
399 | .defaultHint(hint) | ||
400 | .queries(query)) | ||
401 | .build(); | ||
402 | |||
403 | var model = store.createEmptyModel(); | ||
404 | var personInterpretation = model.getInterpretation(person); | ||
405 | var ageInterpretation = model.getInterpretation(age); | ||
406 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
407 | var queryResultSet = queryEngine.getResultSet(query); | ||
408 | |||
409 | personInterpretation.put(Tuple.of(0), true); | ||
410 | personInterpretation.put(Tuple.of(1), true); | ||
411 | personInterpretation.put(Tuple.of(2), true); | ||
412 | |||
413 | ageInterpretation.put(Tuple.of(0), 0); | ||
414 | ageInterpretation.put(Tuple.of(1), 30); | ||
415 | ageInterpretation.put(Tuple.of(2), 20); | ||
416 | |||
417 | queryEngine.flushChanges(); | ||
418 | assertResults(Map.of( | ||
419 | Tuple.of(0), false, | ||
420 | Tuple.of(1), true, | ||
421 | Tuple.of(2), false, | ||
422 | Tuple.of(3), false | ||
423 | ), queryResultSet); | ||
424 | } | ||
425 | |||
426 | @QueryEngineTest | ||
427 | void multipleAssignmentTest(QueryEvaluationHint hint) { | ||
428 | var query = Query.of("MultipleAssignment", Integer.class, (builder, p1, p2, output) -> builder | ||
429 | .clause(Integer.class, Integer.class, (x1, x2) -> List.of( | ||
430 | ageView.call(p1, x1), | ||
431 | ageView.call(p2, x2), | ||
432 | output.assign(mul(x1, constant(2))), | ||
433 | output.assign(mul(x2, constant(3))) | ||
434 | ))); | ||
435 | |||
436 | var store = ModelStore.builder() | ||
437 | .symbols(age) | ||
438 | .with(QueryInterpreterAdapter.builder() | ||
439 | .defaultHint(hint) | ||
440 | .queries(query)) | ||
441 | .build(); | ||
442 | |||
443 | var model = store.createEmptyModel(); | ||
444 | var ageInterpretation = model.getInterpretation(age); | ||
445 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
446 | var queryResultSet = queryEngine.getResultSet(query); | ||
447 | |||
448 | ageInterpretation.put(Tuple.of(0), 3); | ||
449 | ageInterpretation.put(Tuple.of(1), 2); | ||
450 | ageInterpretation.put(Tuple.of(2), 15); | ||
451 | ageInterpretation.put(Tuple.of(3), 10); | ||
452 | |||
453 | queryEngine.flushChanges(); | ||
454 | assertNullableResults(Map.of( | ||
455 | Tuple.of(0, 1), Optional.of(6), | ||
456 | Tuple.of(1, 0), Optional.empty(), | ||
457 | Tuple.of(2, 3), Optional.of(30), | ||
458 | Tuple.of(3, 2), Optional.empty() | ||
459 | ), queryResultSet); | ||
460 | } | ||
461 | |||
462 | @QueryEngineTest | ||
463 | void notFunctionalTest(QueryEvaluationHint hint) { | ||
464 | var query = Query.of("NotFunctional", Integer.class, (builder, p1, output) -> builder.clause((p2) -> List.of( | ||
465 | personView.call(p1), | ||
466 | friendMustView.call(p1, p2), | ||
467 | ageView.call(p2, output) | ||
468 | ))); | ||
469 | |||
470 | var store = ModelStore.builder() | ||
471 | .symbols(person, age, friend) | ||
472 | .with(QueryInterpreterAdapter.builder() | ||
473 | .defaultHint(hint) | ||
474 | .queries(query)) | ||
475 | .build(); | ||
476 | |||
477 | var model = store.createEmptyModel(); | ||
478 | var personInterpretation = model.getInterpretation(person); | ||
479 | var ageInterpretation = model.getInterpretation(age); | ||
480 | var friendInterpretation = model.getInterpretation(friend); | ||
481 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
482 | var queryResultSet = queryEngine.getResultSet(query); | ||
483 | |||
484 | personInterpretation.put(Tuple.of(0), true); | ||
485 | personInterpretation.put(Tuple.of(1), true); | ||
486 | personInterpretation.put(Tuple.of(2), true); | ||
487 | |||
488 | ageInterpretation.put(Tuple.of(0), 24); | ||
489 | ageInterpretation.put(Tuple.of(1), 30); | ||
490 | ageInterpretation.put(Tuple.of(2), 36); | ||
491 | |||
492 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
493 | friendInterpretation.put(Tuple.of(1, 0), TruthValue.TRUE); | ||
494 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
495 | |||
496 | queryEngine.flushChanges(); | ||
497 | var invalidTuple = Tuple.of(1); | ||
498 | var cursor = queryResultSet.getAll(); | ||
499 | assertAll( | ||
500 | () -> assertThat("value for key 0", queryResultSet.get(Tuple.of(0)), is(30)), | ||
501 | () -> assertThrows(IllegalStateException.class, () -> queryResultSet.get(invalidTuple), | ||
502 | "multiple values for key 1"), | ||
503 | () -> assertThat("value for key 2", queryResultSet.get(Tuple.of(2)), is(nullValue())), | ||
504 | () -> assertThat("value for key 3", queryResultSet.get(Tuple.of(3)), is(nullValue())) | ||
505 | ); | ||
506 | if (hint.getQueryBackendRequirementType() != QueryEvaluationHint.BackendRequirement.DEFAULT_SEARCH) { | ||
507 | // Local search doesn't support throwing an error on multiple function return values. | ||
508 | assertThat("results size", queryResultSet.size(), is(2)); | ||
509 | assertThrows(IllegalStateException.class, () -> enumerateValues(cursor), "move cursor"); | ||
510 | } | ||
511 | } | ||
512 | |||
513 | private static void enumerateValues(Cursor<?, ?> cursor) { | ||
514 | //noinspection StatementWithEmptyBody | ||
515 | while (cursor.move()) { | ||
516 | // Nothing do, just let the cursor move through the result set. | ||
517 | } | ||
518 | } | ||
519 | } | ||
diff --git a/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/OrderedResultSetTest.java b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/OrderedResultSetTest.java new file mode 100644 index 00000000..96d0f302 --- /dev/null +++ b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/OrderedResultSetTest.java | |||
@@ -0,0 +1,117 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter; | ||
7 | |||
8 | import org.junit.jupiter.api.Test; | ||
9 | import tools.refinery.store.model.ModelStore; | ||
10 | import tools.refinery.store.query.ModelQueryAdapter; | ||
11 | import tools.refinery.store.query.dnf.Query; | ||
12 | import tools.refinery.store.query.resultset.OrderedResultSet; | ||
13 | import tools.refinery.store.query.term.Variable; | ||
14 | import tools.refinery.store.query.view.AnySymbolView; | ||
15 | import tools.refinery.store.query.view.KeyOnlyView; | ||
16 | import tools.refinery.store.representation.Symbol; | ||
17 | import tools.refinery.store.tuple.Tuple; | ||
18 | |||
19 | import static org.hamcrest.MatcherAssert.assertThat; | ||
20 | import static org.hamcrest.Matchers.is; | ||
21 | |||
22 | class OrderedResultSetTest { | ||
23 | private static final Symbol<Boolean> friend = Symbol.of("friend", 2); | ||
24 | private static final AnySymbolView friendView = new KeyOnlyView<>(friend); | ||
25 | |||
26 | @Test | ||
27 | void relationalFlushTest() { | ||
28 | var query = Query.of("Relation", (builder, p1, p2) -> builder.clause( | ||
29 | friendView.call(p1, p2) | ||
30 | )); | ||
31 | |||
32 | var store = ModelStore.builder() | ||
33 | .symbols(friend) | ||
34 | .with(QueryInterpreterAdapter.builder() | ||
35 | .queries(query)) | ||
36 | .build(); | ||
37 | |||
38 | var model = store.createEmptyModel(); | ||
39 | var friendInterpretation = model.getInterpretation(friend); | ||
40 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
41 | var resultSet = queryEngine.getResultSet(query); | ||
42 | |||
43 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
44 | friendInterpretation.put(Tuple.of(1, 2), true); | ||
45 | friendInterpretation.put(Tuple.of(1, 1), true); | ||
46 | queryEngine.flushChanges(); | ||
47 | |||
48 | try (var orderedResultSet = new OrderedResultSet<>(resultSet)) { | ||
49 | assertThat(orderedResultSet.size(), is(3)); | ||
50 | assertThat(orderedResultSet.getKey(0), is(Tuple.of(0, 1))); | ||
51 | assertThat(orderedResultSet.getKey(1), is(Tuple.of(1, 1))); | ||
52 | assertThat(orderedResultSet.getKey(2), is(Tuple.of(1, 2))); | ||
53 | |||
54 | friendInterpretation.put(Tuple.of(1, 2), false); | ||
55 | friendInterpretation.put(Tuple.of(0, 2), true); | ||
56 | queryEngine.flushChanges(); | ||
57 | |||
58 | assertThat(orderedResultSet.size(), is(3)); | ||
59 | assertThat(orderedResultSet.getKey(0), is(Tuple.of(0, 1))); | ||
60 | assertThat(orderedResultSet.getKey(1), is(Tuple.of(0, 2))); | ||
61 | assertThat(orderedResultSet.getKey(2), is(Tuple.of(1, 1))); | ||
62 | } | ||
63 | } | ||
64 | |||
65 | @Test | ||
66 | void functionalFlushTest() { | ||
67 | var query = Query.of("Function", Integer.class, (builder, p1, output) -> builder.clause( | ||
68 | friendView.call(p1, Variable.of()), | ||
69 | output.assign(friendView.count(p1, Variable.of())) | ||
70 | )); | ||
71 | |||
72 | var store = ModelStore.builder() | ||
73 | .symbols(friend) | ||
74 | .with(QueryInterpreterAdapter.builder() | ||
75 | .queries(query)) | ||
76 | .build(); | ||
77 | |||
78 | var model = store.createEmptyModel(); | ||
79 | var friendInterpretation = model.getInterpretation(friend); | ||
80 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
81 | var resultSet = queryEngine.getResultSet(query); | ||
82 | |||
83 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
84 | friendInterpretation.put(Tuple.of(1, 2), true); | ||
85 | friendInterpretation.put(Tuple.of(1, 1), true); | ||
86 | queryEngine.flushChanges(); | ||
87 | |||
88 | try (var orderedResultSet = new OrderedResultSet<>(resultSet)) { | ||
89 | assertThat(orderedResultSet.size(), is(2)); | ||
90 | assertThat(orderedResultSet.getKey(0), is(Tuple.of(0))); | ||
91 | assertThat(orderedResultSet.getKey(1), is(Tuple.of(1))); | ||
92 | |||
93 | friendInterpretation.put(Tuple.of(0, 1), false); | ||
94 | friendInterpretation.put(Tuple.of(2, 1), true); | ||
95 | queryEngine.flushChanges(); | ||
96 | |||
97 | assertThat(orderedResultSet.size(), is(2)); | ||
98 | assertThat(orderedResultSet.getKey(0), is(Tuple.of(1))); | ||
99 | assertThat(orderedResultSet.getKey(1), is(Tuple.of(2))); | ||
100 | |||
101 | friendInterpretation.put(Tuple.of(1, 1), false); | ||
102 | queryEngine.flushChanges(); | ||
103 | |||
104 | assertThat(orderedResultSet.size(), is(2)); | ||
105 | assertThat(orderedResultSet.getKey(0), is(Tuple.of(1))); | ||
106 | assertThat(orderedResultSet.getKey(1), is(Tuple.of(2))); | ||
107 | |||
108 | friendInterpretation.put(Tuple.of(1, 2), false); | ||
109 | friendInterpretation.put(Tuple.of(1, 0), true); | ||
110 | queryEngine.flushChanges(); | ||
111 | |||
112 | assertThat(orderedResultSet.size(), is(2)); | ||
113 | assertThat(orderedResultSet.getKey(0), is(Tuple.of(1))); | ||
114 | assertThat(orderedResultSet.getKey(1), is(Tuple.of(2))); | ||
115 | } | ||
116 | } | ||
117 | } | ||
diff --git a/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/QueryTest.java b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/QueryTest.java new file mode 100644 index 00000000..72728dcd --- /dev/null +++ b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/QueryTest.java | |||
@@ -0,0 +1,794 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.backend.QueryEvaluationHint; | ||
9 | import org.junit.jupiter.api.Test; | ||
10 | import tools.refinery.store.model.ModelStore; | ||
11 | import tools.refinery.store.query.ModelQueryAdapter; | ||
12 | import tools.refinery.store.query.dnf.Dnf; | ||
13 | import tools.refinery.store.query.dnf.Query; | ||
14 | import tools.refinery.store.query.term.ParameterDirection; | ||
15 | import tools.refinery.store.query.term.Variable; | ||
16 | import tools.refinery.store.query.interpreter.tests.QueryEngineTest; | ||
17 | import tools.refinery.store.query.view.AnySymbolView; | ||
18 | import tools.refinery.store.query.view.FilteredView; | ||
19 | import tools.refinery.store.query.view.FunctionView; | ||
20 | import tools.refinery.store.query.view.KeyOnlyView; | ||
21 | import tools.refinery.store.representation.Symbol; | ||
22 | import tools.refinery.store.representation.TruthValue; | ||
23 | import tools.refinery.store.tuple.Tuple; | ||
24 | |||
25 | import java.util.List; | ||
26 | import java.util.Map; | ||
27 | |||
28 | import static tools.refinery.store.query.literal.Literals.check; | ||
29 | import static tools.refinery.store.query.literal.Literals.not; | ||
30 | import static tools.refinery.store.query.term.int_.IntTerms.constant; | ||
31 | import static tools.refinery.store.query.term.int_.IntTerms.greaterEq; | ||
32 | import static tools.refinery.store.query.interpreter.tests.QueryAssertions.assertResults; | ||
33 | |||
34 | class QueryTest { | ||
35 | private static final Symbol<Boolean> person = Symbol.of("Person", 1); | ||
36 | private static final Symbol<TruthValue> friend = Symbol.of("friend", 2, TruthValue.class, TruthValue.FALSE); | ||
37 | private static final AnySymbolView personView = new KeyOnlyView<>(person); | ||
38 | private static final AnySymbolView friendMustView = new FilteredView<>(friend, "must", TruthValue::must); | ||
39 | |||
40 | @QueryEngineTest | ||
41 | void typeConstraintTest(QueryEvaluationHint hint) { | ||
42 | var asset = Symbol.of("Asset", 1); | ||
43 | |||
44 | var predicate = Query.of("TypeConstraint", (builder, p1) -> builder.clause(personView.call(p1))); | ||
45 | |||
46 | var store = ModelStore.builder() | ||
47 | .symbols(person, asset) | ||
48 | .with(QueryInterpreterAdapter.builder() | ||
49 | .defaultHint(hint) | ||
50 | .queries(predicate)) | ||
51 | .build(); | ||
52 | |||
53 | var model = store.createEmptyModel(); | ||
54 | var personInterpretation = model.getInterpretation(person); | ||
55 | var assetInterpretation = model.getInterpretation(asset); | ||
56 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
57 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
58 | |||
59 | personInterpretation.put(Tuple.of(0), true); | ||
60 | personInterpretation.put(Tuple.of(1), true); | ||
61 | |||
62 | assetInterpretation.put(Tuple.of(1), true); | ||
63 | assetInterpretation.put(Tuple.of(2), true); | ||
64 | |||
65 | queryEngine.flushChanges(); | ||
66 | assertResults(Map.of( | ||
67 | Tuple.of(0), true, | ||
68 | Tuple.of(1), true, | ||
69 | Tuple.of(2), false | ||
70 | ), predicateResultSet); | ||
71 | } | ||
72 | |||
73 | @QueryEngineTest | ||
74 | void relationConstraintTest(QueryEvaluationHint hint) { | ||
75 | var predicate = Query.of("RelationConstraint", (builder, p1, p2) -> builder.clause( | ||
76 | personView.call(p1), | ||
77 | personView.call(p2), | ||
78 | friendMustView.call(p1, p2) | ||
79 | )); | ||
80 | |||
81 | var store = ModelStore.builder() | ||
82 | .symbols(person, friend) | ||
83 | .with(QueryInterpreterAdapter.builder() | ||
84 | .defaultHint(hint) | ||
85 | .queries(predicate)) | ||
86 | .build(); | ||
87 | |||
88 | var model = store.createEmptyModel(); | ||
89 | var personInterpretation = model.getInterpretation(person); | ||
90 | var friendInterpretation = model.getInterpretation(friend); | ||
91 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
92 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
93 | |||
94 | personInterpretation.put(Tuple.of(0), true); | ||
95 | personInterpretation.put(Tuple.of(1), true); | ||
96 | personInterpretation.put(Tuple.of(2), true); | ||
97 | |||
98 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
99 | friendInterpretation.put(Tuple.of(1, 0), TruthValue.TRUE); | ||
100 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
101 | friendInterpretation.put(Tuple.of(1, 3), TruthValue.TRUE); | ||
102 | |||
103 | queryEngine.flushChanges(); | ||
104 | assertResults(Map.of( | ||
105 | Tuple.of(0, 1), true, | ||
106 | Tuple.of(1, 0), true, | ||
107 | Tuple.of(1, 2), true, | ||
108 | Tuple.of(2, 1), false | ||
109 | ), predicateResultSet); | ||
110 | } | ||
111 | |||
112 | @QueryEngineTest | ||
113 | void isConstantTest(QueryEvaluationHint hint) { | ||
114 | var predicate = Query.of("RelationConstraint", (builder, p1, p2) -> builder.clause( | ||
115 | personView.call(p1), | ||
116 | p1.isConstant(1), | ||
117 | friendMustView.call(p1, p2) | ||
118 | )); | ||
119 | |||
120 | var store = ModelStore.builder() | ||
121 | .symbols(person, friend) | ||
122 | .with(QueryInterpreterAdapter.builder() | ||
123 | .defaultHint(hint) | ||
124 | .queries(predicate)) | ||
125 | .build(); | ||
126 | |||
127 | var model = store.createEmptyModel(); | ||
128 | var personInterpretation = model.getInterpretation(person); | ||
129 | var friendInterpretation = model.getInterpretation(friend); | ||
130 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
131 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
132 | |||
133 | personInterpretation.put(Tuple.of(0), true); | ||
134 | personInterpretation.put(Tuple.of(1), true); | ||
135 | personInterpretation.put(Tuple.of(2), true); | ||
136 | |||
137 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
138 | friendInterpretation.put(Tuple.of(1, 0), TruthValue.TRUE); | ||
139 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
140 | |||
141 | queryEngine.flushChanges(); | ||
142 | assertResults(Map.of( | ||
143 | Tuple.of(0, 1), false, | ||
144 | Tuple.of(1, 0), true, | ||
145 | Tuple.of(1, 2), true, | ||
146 | Tuple.of(2, 1), false | ||
147 | ), predicateResultSet); | ||
148 | } | ||
149 | |||
150 | @QueryEngineTest | ||
151 | void existTest(QueryEvaluationHint hint) { | ||
152 | var predicate = Query.of("Exists", (builder, p1) -> builder.clause((p2) -> List.of( | ||
153 | personView.call(p1), | ||
154 | personView.call(p2), | ||
155 | friendMustView.call(p1, p2) | ||
156 | ))); | ||
157 | |||
158 | var store = ModelStore.builder() | ||
159 | .symbols(person, friend) | ||
160 | .with(QueryInterpreterAdapter.builder() | ||
161 | .defaultHint(hint) | ||
162 | .queries(predicate)) | ||
163 | .build(); | ||
164 | |||
165 | var model = store.createEmptyModel(); | ||
166 | var personInterpretation = model.getInterpretation(person); | ||
167 | var friendInterpretation = model.getInterpretation(friend); | ||
168 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
169 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
170 | |||
171 | personInterpretation.put(Tuple.of(0), true); | ||
172 | personInterpretation.put(Tuple.of(1), true); | ||
173 | personInterpretation.put(Tuple.of(2), true); | ||
174 | |||
175 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
176 | friendInterpretation.put(Tuple.of(1, 0), TruthValue.TRUE); | ||
177 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
178 | friendInterpretation.put(Tuple.of(3, 2), TruthValue.TRUE); | ||
179 | |||
180 | queryEngine.flushChanges(); | ||
181 | assertResults(Map.of( | ||
182 | Tuple.of(0), true, | ||
183 | Tuple.of(1), true, | ||
184 | Tuple.of(2), false, | ||
185 | Tuple.of(3), false | ||
186 | ), predicateResultSet); | ||
187 | } | ||
188 | |||
189 | @QueryEngineTest | ||
190 | void orTest(QueryEvaluationHint hint) { | ||
191 | var animal = Symbol.of("Animal", 1); | ||
192 | var animalView = new KeyOnlyView<>(animal); | ||
193 | |||
194 | var predicate = Query.of("Or", (builder, p1, p2) -> builder.clause( | ||
195 | personView.call(p1), | ||
196 | personView.call(p2), | ||
197 | friendMustView.call(p1, p2) | ||
198 | ).clause( | ||
199 | animalView.call(p1), | ||
200 | animalView.call(p2), | ||
201 | friendMustView.call(p1, p2) | ||
202 | )); | ||
203 | |||
204 | var store = ModelStore.builder() | ||
205 | .symbols(person, animal, friend) | ||
206 | .with(QueryInterpreterAdapter.builder() | ||
207 | .defaultHint(hint) | ||
208 | .queries(predicate)) | ||
209 | .build(); | ||
210 | |||
211 | var model = store.createEmptyModel(); | ||
212 | var personInterpretation = model.getInterpretation(person); | ||
213 | var animalInterpretation = model.getInterpretation(animal); | ||
214 | var friendInterpretation = model.getInterpretation(friend); | ||
215 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
216 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
217 | |||
218 | personInterpretation.put(Tuple.of(0), true); | ||
219 | personInterpretation.put(Tuple.of(1), true); | ||
220 | |||
221 | animalInterpretation.put(Tuple.of(2), true); | ||
222 | animalInterpretation.put(Tuple.of(3), true); | ||
223 | |||
224 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
225 | friendInterpretation.put(Tuple.of(0, 2), TruthValue.TRUE); | ||
226 | friendInterpretation.put(Tuple.of(2, 3), TruthValue.TRUE); | ||
227 | friendInterpretation.put(Tuple.of(3, 0), TruthValue.TRUE); | ||
228 | |||
229 | queryEngine.flushChanges(); | ||
230 | assertResults(Map.of( | ||
231 | Tuple.of(0, 1), true, | ||
232 | Tuple.of(0, 2), false, | ||
233 | Tuple.of(2, 3), true, | ||
234 | Tuple.of(3, 0), false, | ||
235 | Tuple.of(3, 2), false | ||
236 | ), predicateResultSet); | ||
237 | } | ||
238 | |||
239 | @QueryEngineTest | ||
240 | void equalityTest(QueryEvaluationHint hint) { | ||
241 | var predicate = Query.of("Equality", (builder, p1, p2) -> builder.clause( | ||
242 | personView.call(p1), | ||
243 | personView.call(p2), | ||
244 | p1.isEquivalent(p2) | ||
245 | )); | ||
246 | |||
247 | var store = ModelStore.builder() | ||
248 | .symbols(person) | ||
249 | .with(QueryInterpreterAdapter.builder() | ||
250 | .defaultHint(hint) | ||
251 | .queries(predicate)) | ||
252 | .build(); | ||
253 | |||
254 | var model = store.createEmptyModel(); | ||
255 | var personInterpretation = model.getInterpretation(person); | ||
256 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
257 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
258 | |||
259 | personInterpretation.put(Tuple.of(0), true); | ||
260 | personInterpretation.put(Tuple.of(1), true); | ||
261 | personInterpretation.put(Tuple.of(2), true); | ||
262 | |||
263 | queryEngine.flushChanges(); | ||
264 | assertResults(Map.of( | ||
265 | Tuple.of(0, 0), true, | ||
266 | Tuple.of(1, 1), true, | ||
267 | Tuple.of(2, 2), true, | ||
268 | Tuple.of(0, 1), false, | ||
269 | Tuple.of(3, 3), false | ||
270 | ), predicateResultSet); | ||
271 | } | ||
272 | |||
273 | @QueryEngineTest | ||
274 | void inequalityTest(QueryEvaluationHint hint) { | ||
275 | var predicate = Query.of("Inequality", (builder, p1, p2, p3) -> builder.clause( | ||
276 | personView.call(p1), | ||
277 | personView.call(p2), | ||
278 | friendMustView.call(p1, p3), | ||
279 | friendMustView.call(p2, p3), | ||
280 | p1.notEquivalent(p2) | ||
281 | )); | ||
282 | |||
283 | var store = ModelStore.builder() | ||
284 | .symbols(person, friend) | ||
285 | .with(QueryInterpreterAdapter.builder() | ||
286 | .defaultHint(hint) | ||
287 | .queries(predicate)) | ||
288 | .build(); | ||
289 | |||
290 | var model = store.createEmptyModel(); | ||
291 | var personInterpretation = model.getInterpretation(person); | ||
292 | var friendInterpretation = model.getInterpretation(friend); | ||
293 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
294 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
295 | |||
296 | personInterpretation.put(Tuple.of(0), true); | ||
297 | personInterpretation.put(Tuple.of(1), true); | ||
298 | personInterpretation.put(Tuple.of(2), true); | ||
299 | |||
300 | friendInterpretation.put(Tuple.of(0, 2), TruthValue.TRUE); | ||
301 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
302 | |||
303 | queryEngine.flushChanges(); | ||
304 | assertResults(Map.of( | ||
305 | Tuple.of(0, 1, 2), true, | ||
306 | Tuple.of(1, 0, 2), true, | ||
307 | Tuple.of(0, 0, 2), false | ||
308 | ), predicateResultSet); | ||
309 | } | ||
310 | |||
311 | @QueryEngineTest | ||
312 | void patternCallTest(QueryEvaluationHint hint) { | ||
313 | var friendPredicate = Query.of("Friend", (builder, p1, p2) -> builder.clause( | ||
314 | personView.call(p1), | ||
315 | personView.call(p2), | ||
316 | friendMustView.call(p1, p2) | ||
317 | )); | ||
318 | var predicate = Query.of("PositivePatternCall", (builder, p3, p4) -> builder.clause( | ||
319 | personView.call(p3), | ||
320 | personView.call(p4), | ||
321 | friendPredicate.call(p3, p4) | ||
322 | )); | ||
323 | |||
324 | var store = ModelStore.builder() | ||
325 | .symbols(person, friend) | ||
326 | .with(QueryInterpreterAdapter.builder() | ||
327 | .defaultHint(hint) | ||
328 | .queries(predicate)) | ||
329 | .build(); | ||
330 | |||
331 | var model = store.createEmptyModel(); | ||
332 | var personInterpretation = model.getInterpretation(person); | ||
333 | var friendInterpretation = model.getInterpretation(friend); | ||
334 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
335 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
336 | |||
337 | personInterpretation.put(Tuple.of(0), true); | ||
338 | personInterpretation.put(Tuple.of(1), true); | ||
339 | personInterpretation.put(Tuple.of(2), true); | ||
340 | |||
341 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
342 | friendInterpretation.put(Tuple.of(1, 0), TruthValue.TRUE); | ||
343 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
344 | |||
345 | queryEngine.flushChanges(); | ||
346 | assertResults(Map.of( | ||
347 | Tuple.of(0, 1), true, | ||
348 | Tuple.of(1, 0), true, | ||
349 | Tuple.of(1, 2), true, | ||
350 | Tuple.of(2, 1), false | ||
351 | ), predicateResultSet); | ||
352 | } | ||
353 | |||
354 | @QueryEngineTest | ||
355 | void patternCallInputArgumentTest(QueryEvaluationHint hint) { | ||
356 | var friendPredicate = Dnf.of("Friend", builder -> { | ||
357 | var p1 = builder.parameter("p1", ParameterDirection.IN); | ||
358 | var p2 = builder.parameter("p2", ParameterDirection.IN); | ||
359 | builder.clause( | ||
360 | personView.call(p1), | ||
361 | personView.call(p2), | ||
362 | friendMustView.call(p1, p2) | ||
363 | ); | ||
364 | }); | ||
365 | var predicate = Query.of("PositivePatternCall", (builder, p3, p4) -> builder.clause( | ||
366 | personView.call(p3), | ||
367 | personView.call(p4), | ||
368 | friendPredicate.call(p3, p4) | ||
369 | )); | ||
370 | |||
371 | var store = ModelStore.builder() | ||
372 | .symbols(person, friend) | ||
373 | .with(QueryInterpreterAdapter.builder() | ||
374 | .defaultHint(hint) | ||
375 | .queries(predicate)) | ||
376 | .build(); | ||
377 | |||
378 | var model = store.createEmptyModel(); | ||
379 | var personInterpretation = model.getInterpretation(person); | ||
380 | var friendInterpretation = model.getInterpretation(friend); | ||
381 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
382 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
383 | |||
384 | personInterpretation.put(Tuple.of(0), true); | ||
385 | personInterpretation.put(Tuple.of(1), true); | ||
386 | personInterpretation.put(Tuple.of(2), true); | ||
387 | |||
388 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
389 | friendInterpretation.put(Tuple.of(1, 0), TruthValue.TRUE); | ||
390 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
391 | |||
392 | queryEngine.flushChanges(); | ||
393 | assertResults(Map.of( | ||
394 | Tuple.of(0, 1), true, | ||
395 | Tuple.of(1, 0), true, | ||
396 | Tuple.of(1, 2), true, | ||
397 | Tuple.of(2, 1), false | ||
398 | ), predicateResultSet); | ||
399 | } | ||
400 | |||
401 | @QueryEngineTest | ||
402 | void negativeRelationViewTest(QueryEvaluationHint hint) { | ||
403 | var predicate = Query.of("NegativePatternCall", (builder, p1, p2) -> builder.clause( | ||
404 | personView.call(p1), | ||
405 | personView.call(p2), | ||
406 | not(friendMustView.call(p1, p2)) | ||
407 | )); | ||
408 | |||
409 | var store = ModelStore.builder() | ||
410 | .symbols(person, friend) | ||
411 | .with(QueryInterpreterAdapter.builder() | ||
412 | .defaultHint(hint) | ||
413 | .queries(predicate)) | ||
414 | .build(); | ||
415 | |||
416 | var model = store.createEmptyModel(); | ||
417 | var personInterpretation = model.getInterpretation(person); | ||
418 | var friendInterpretation = model.getInterpretation(friend); | ||
419 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
420 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
421 | |||
422 | personInterpretation.put(Tuple.of(0), true); | ||
423 | personInterpretation.put(Tuple.of(1), true); | ||
424 | personInterpretation.put(Tuple.of(2), true); | ||
425 | |||
426 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
427 | friendInterpretation.put(Tuple.of(1, 0), TruthValue.TRUE); | ||
428 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
429 | |||
430 | queryEngine.flushChanges(); | ||
431 | assertResults(Map.of( | ||
432 | Tuple.of(0, 0), true, | ||
433 | Tuple.of(0, 2), true, | ||
434 | Tuple.of(1, 1), true, | ||
435 | Tuple.of(2, 0), true, | ||
436 | Tuple.of(2, 1), true, | ||
437 | Tuple.of(2, 2), true, | ||
438 | Tuple.of(0, 1), false, | ||
439 | Tuple.of(1, 0), false, | ||
440 | Tuple.of(1, 2), false, | ||
441 | Tuple.of(0, 3), false | ||
442 | ), predicateResultSet); | ||
443 | } | ||
444 | |||
445 | @QueryEngineTest | ||
446 | void negativePatternCallTest(QueryEvaluationHint hint) { | ||
447 | var friendPredicate = Query.of("Friend", (builder, p1, p2) -> builder.clause( | ||
448 | personView.call(p1), | ||
449 | personView.call(p2), | ||
450 | friendMustView.call(p1, p2) | ||
451 | )); | ||
452 | var predicate = Query.of("NegativePatternCall", (builder, p3, p4) -> builder.clause( | ||
453 | personView.call(p3), | ||
454 | personView.call(p4), | ||
455 | not(friendPredicate.call(p3, p4)) | ||
456 | )); | ||
457 | |||
458 | var store = ModelStore.builder() | ||
459 | .symbols(person, friend) | ||
460 | .with(QueryInterpreterAdapter.builder() | ||
461 | .defaultHint(hint) | ||
462 | .queries(predicate)) | ||
463 | .build(); | ||
464 | |||
465 | var model = store.createEmptyModel(); | ||
466 | var personInterpretation = model.getInterpretation(person); | ||
467 | var friendInterpretation = model.getInterpretation(friend); | ||
468 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
469 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
470 | |||
471 | personInterpretation.put(Tuple.of(0), true); | ||
472 | personInterpretation.put(Tuple.of(1), true); | ||
473 | personInterpretation.put(Tuple.of(2), true); | ||
474 | |||
475 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
476 | friendInterpretation.put(Tuple.of(1, 0), TruthValue.TRUE); | ||
477 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
478 | |||
479 | queryEngine.flushChanges(); | ||
480 | assertResults(Map.of( | ||
481 | Tuple.of(0, 0), true, | ||
482 | Tuple.of(0, 2), true, | ||
483 | Tuple.of(1, 1), true, | ||
484 | Tuple.of(2, 0), true, | ||
485 | Tuple.of(2, 1), true, | ||
486 | Tuple.of(2, 2), true, | ||
487 | Tuple.of(0, 1), false, | ||
488 | Tuple.of(1, 0), false, | ||
489 | Tuple.of(1, 2), false, | ||
490 | Tuple.of(0, 3), false | ||
491 | ), predicateResultSet); | ||
492 | } | ||
493 | |||
494 | @QueryEngineTest | ||
495 | void negativeRelationViewWithQuantificationTest(QueryEvaluationHint hint) { | ||
496 | var predicate = Query.of("Negative", (builder, p1) -> builder.clause( | ||
497 | personView.call(p1), | ||
498 | not(friendMustView.call(p1, Variable.of())) | ||
499 | )); | ||
500 | |||
501 | var store = ModelStore.builder() | ||
502 | .symbols(person, friend) | ||
503 | .with(QueryInterpreterAdapter.builder() | ||
504 | .defaultHint(hint) | ||
505 | .queries(predicate)) | ||
506 | .build(); | ||
507 | |||
508 | var model = store.createEmptyModel(); | ||
509 | var personInterpretation = model.getInterpretation(person); | ||
510 | var friendInterpretation = model.getInterpretation(friend); | ||
511 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
512 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
513 | |||
514 | personInterpretation.put(Tuple.of(0), true); | ||
515 | personInterpretation.put(Tuple.of(1), true); | ||
516 | personInterpretation.put(Tuple.of(2), true); | ||
517 | |||
518 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
519 | friendInterpretation.put(Tuple.of(0, 2), TruthValue.TRUE); | ||
520 | |||
521 | queryEngine.flushChanges(); | ||
522 | assertResults(Map.of( | ||
523 | Tuple.of(0), false, | ||
524 | Tuple.of(1), true, | ||
525 | Tuple.of(2), true, | ||
526 | Tuple.of(3), false | ||
527 | ), predicateResultSet); | ||
528 | } | ||
529 | |||
530 | @QueryEngineTest | ||
531 | void negativeWithQuantificationTest(QueryEvaluationHint hint) { | ||
532 | var called = Query.of("Called", (builder, p1, p2) -> builder.clause( | ||
533 | personView.call(p1), | ||
534 | personView.call(p2), | ||
535 | friendMustView.call(p1, p2) | ||
536 | )); | ||
537 | var predicate = Query.of("Negative", (builder, p1) -> builder.clause( | ||
538 | personView.call(p1), | ||
539 | not(called.call(p1, Variable.of())) | ||
540 | )); | ||
541 | |||
542 | var store = ModelStore.builder() | ||
543 | .symbols(person, friend) | ||
544 | .with(QueryInterpreterAdapter.builder() | ||
545 | .defaultHint(hint) | ||
546 | .queries(predicate)) | ||
547 | .build(); | ||
548 | |||
549 | var model = store.createEmptyModel(); | ||
550 | var personInterpretation = model.getInterpretation(person); | ||
551 | var friendInterpretation = model.getInterpretation(friend); | ||
552 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
553 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
554 | |||
555 | personInterpretation.put(Tuple.of(0), true); | ||
556 | personInterpretation.put(Tuple.of(1), true); | ||
557 | personInterpretation.put(Tuple.of(2), true); | ||
558 | |||
559 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
560 | friendInterpretation.put(Tuple.of(0, 2), TruthValue.TRUE); | ||
561 | |||
562 | queryEngine.flushChanges(); | ||
563 | assertResults(Map.of( | ||
564 | Tuple.of(0), false, | ||
565 | Tuple.of(1), true, | ||
566 | Tuple.of(2), true, | ||
567 | Tuple.of(3), false | ||
568 | ), predicateResultSet); | ||
569 | } | ||
570 | |||
571 | @QueryEngineTest | ||
572 | void transitiveRelationViewTest(QueryEvaluationHint hint) { | ||
573 | var predicate = Query.of("Transitive", (builder, p1, p2) -> builder.clause( | ||
574 | personView.call(p1), | ||
575 | personView.call(p2), | ||
576 | friendMustView.callTransitive(p1, p2) | ||
577 | )); | ||
578 | |||
579 | var store = ModelStore.builder() | ||
580 | .symbols(person, friend) | ||
581 | .with(QueryInterpreterAdapter.builder() | ||
582 | .defaultHint(hint) | ||
583 | .queries(predicate)) | ||
584 | .build(); | ||
585 | |||
586 | var model = store.createEmptyModel(); | ||
587 | var personInterpretation = model.getInterpretation(person); | ||
588 | var friendInterpretation = model.getInterpretation(friend); | ||
589 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
590 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
591 | |||
592 | personInterpretation.put(Tuple.of(0), true); | ||
593 | personInterpretation.put(Tuple.of(1), true); | ||
594 | personInterpretation.put(Tuple.of(2), true); | ||
595 | |||
596 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
597 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
598 | |||
599 | queryEngine.flushChanges(); | ||
600 | assertResults(Map.of( | ||
601 | Tuple.of(0, 0), false, | ||
602 | Tuple.of(0, 1), true, | ||
603 | Tuple.of(0, 2), true, | ||
604 | Tuple.of(1, 0), false, | ||
605 | Tuple.of(1, 1), false, | ||
606 | Tuple.of(1, 2), true, | ||
607 | Tuple.of(2, 0), false, | ||
608 | Tuple.of(2, 1), false, | ||
609 | Tuple.of(2, 2), false, | ||
610 | Tuple.of(2, 3), false | ||
611 | ), predicateResultSet); | ||
612 | } | ||
613 | |||
614 | @QueryEngineTest | ||
615 | void transitivePatternCallTest(QueryEvaluationHint hint) { | ||
616 | var called = Query.of("Called", (builder, p1, p2) -> builder.clause( | ||
617 | personView.call(p1), | ||
618 | personView.call(p2), | ||
619 | friendMustView.call(p1, p2) | ||
620 | )); | ||
621 | var predicate = Query.of("Transitive", (builder, p1, p2) -> builder.clause( | ||
622 | personView.call(p1), | ||
623 | personView.call(p2), | ||
624 | called.callTransitive(p1, p2) | ||
625 | )); | ||
626 | |||
627 | var store = ModelStore.builder() | ||
628 | .symbols(person, friend) | ||
629 | .with(QueryInterpreterAdapter.builder() | ||
630 | .defaultHint(hint) | ||
631 | .queries(predicate)) | ||
632 | .build(); | ||
633 | |||
634 | var model = store.createEmptyModel(); | ||
635 | var personInterpretation = model.getInterpretation(person); | ||
636 | var friendInterpretation = model.getInterpretation(friend); | ||
637 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
638 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
639 | |||
640 | personInterpretation.put(Tuple.of(0), true); | ||
641 | personInterpretation.put(Tuple.of(1), true); | ||
642 | personInterpretation.put(Tuple.of(2), true); | ||
643 | |||
644 | friendInterpretation.put(Tuple.of(0, 1), TruthValue.TRUE); | ||
645 | friendInterpretation.put(Tuple.of(1, 2), TruthValue.TRUE); | ||
646 | |||
647 | queryEngine.flushChanges(); | ||
648 | assertResults(Map.of( | ||
649 | Tuple.of(0, 0), false, | ||
650 | Tuple.of(0, 1), true, | ||
651 | Tuple.of(0, 2), true, | ||
652 | Tuple.of(1, 0), false, | ||
653 | Tuple.of(1, 1), false, | ||
654 | Tuple.of(1, 2), true, | ||
655 | Tuple.of(2, 0), false, | ||
656 | Tuple.of(2, 1), false, | ||
657 | Tuple.of(2, 2), false, | ||
658 | Tuple.of(2, 3), false | ||
659 | ), predicateResultSet); | ||
660 | } | ||
661 | |||
662 | @Test | ||
663 | void filteredIntegerViewTest() { | ||
664 | var distance = Symbol.of("distance", 2, Integer.class); | ||
665 | var nearView = new FilteredView<>(distance, value -> value < 2); | ||
666 | var farView = new FilteredView<>(distance, value -> value >= 5); | ||
667 | var dangerQuery = Query.of("danger", (builder, a1, a2) -> builder.clause((a3) -> List.of( | ||
668 | a1.notEquivalent(a2), | ||
669 | nearView.call(a1, a3), | ||
670 | nearView.call(a2, a3), | ||
671 | not(farView.call(a1, a2)) | ||
672 | ))); | ||
673 | var store = ModelStore.builder() | ||
674 | .symbols(distance) | ||
675 | .with(QueryInterpreterAdapter.builder() | ||
676 | .queries(dangerQuery)) | ||
677 | .build(); | ||
678 | |||
679 | var model = store.createEmptyModel(); | ||
680 | var distanceInterpretation = model.getInterpretation(distance); | ||
681 | distanceInterpretation.put(Tuple.of(0, 1), 1); | ||
682 | distanceInterpretation.put(Tuple.of(1, 0), 1); | ||
683 | distanceInterpretation.put(Tuple.of(0, 2), 1); | ||
684 | distanceInterpretation.put(Tuple.of(2, 0), 1); | ||
685 | distanceInterpretation.put(Tuple.of(1, 2), 3); | ||
686 | distanceInterpretation.put(Tuple.of(2, 1), 3); | ||
687 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
688 | var dangerResultSet = queryEngine.getResultSet(dangerQuery); | ||
689 | queryEngine.flushChanges(); | ||
690 | assertResults(Map.of( | ||
691 | Tuple.of(0, 1), false, | ||
692 | Tuple.of(0, 2), false, | ||
693 | Tuple.of(1, 2), true, | ||
694 | Tuple.of(2, 1), true | ||
695 | ), dangerResultSet); | ||
696 | } | ||
697 | |||
698 | @Test | ||
699 | void filteredDoubleViewTest() { | ||
700 | var distance = Symbol.of("distance", 2, Double.class); | ||
701 | var nearView = new FilteredView<>(distance, value -> value < 2); | ||
702 | var farView = new FilteredView<>(distance, value -> value >= 5); | ||
703 | var dangerQuery = Query.of("danger", (builder, a1, a2) -> builder.clause((a3) -> List.of( | ||
704 | a1.notEquivalent(a2), | ||
705 | nearView.call(a1, a3), | ||
706 | nearView.call(a2, a3), | ||
707 | not(farView.call(a1, a2)) | ||
708 | ))); | ||
709 | var store = ModelStore.builder() | ||
710 | .symbols(distance) | ||
711 | .with(QueryInterpreterAdapter.builder() | ||
712 | .queries(dangerQuery)) | ||
713 | .build(); | ||
714 | |||
715 | var model = store.createEmptyModel(); | ||
716 | var distanceInterpretation = model.getInterpretation(distance); | ||
717 | distanceInterpretation.put(Tuple.of(0, 1), 1.0); | ||
718 | distanceInterpretation.put(Tuple.of(1, 0), 1.0); | ||
719 | distanceInterpretation.put(Tuple.of(0, 2), 1.0); | ||
720 | distanceInterpretation.put(Tuple.of(2, 0), 1.0); | ||
721 | distanceInterpretation.put(Tuple.of(1, 2), 3.0); | ||
722 | distanceInterpretation.put(Tuple.of(2, 1), 3.0); | ||
723 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
724 | var dangerResultSet = queryEngine.getResultSet(dangerQuery); | ||
725 | queryEngine.flushChanges(); | ||
726 | assertResults(Map.of( | ||
727 | Tuple.of(0, 1), false, | ||
728 | Tuple.of(0, 2), false, | ||
729 | Tuple.of(1, 2), true, | ||
730 | Tuple.of(2, 1), true | ||
731 | ), dangerResultSet); | ||
732 | } | ||
733 | |||
734 | @QueryEngineTest | ||
735 | void assumeTest(QueryEvaluationHint hint) { | ||
736 | var age = Symbol.of("age", 1, Integer.class); | ||
737 | var ageView = new FunctionView<>(age); | ||
738 | |||
739 | var query = Query.of("Constraint", (builder, p1) -> builder.clause(Integer.class, (x) -> List.of( | ||
740 | personView.call(p1), | ||
741 | ageView.call(p1, x), | ||
742 | check(greaterEq(x, constant(18))) | ||
743 | ))); | ||
744 | |||
745 | var store = ModelStore.builder() | ||
746 | .symbols(person, age) | ||
747 | .with(QueryInterpreterAdapter.builder() | ||
748 | .defaultHint(hint) | ||
749 | .queries(query)) | ||
750 | .build(); | ||
751 | |||
752 | var model = store.createEmptyModel(); | ||
753 | var personInterpretation = model.getInterpretation(person); | ||
754 | var ageInterpretation = model.getInterpretation(age); | ||
755 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
756 | var queryResultSet = queryEngine.getResultSet(query); | ||
757 | |||
758 | personInterpretation.put(Tuple.of(0), true); | ||
759 | personInterpretation.put(Tuple.of(1), true); | ||
760 | |||
761 | ageInterpretation.put(Tuple.of(0), 12); | ||
762 | ageInterpretation.put(Tuple.of(1), 24); | ||
763 | |||
764 | queryEngine.flushChanges(); | ||
765 | assertResults(Map.of( | ||
766 | Tuple.of(0), false, | ||
767 | Tuple.of(1), true, | ||
768 | Tuple.of(2), false | ||
769 | ), queryResultSet); | ||
770 | } | ||
771 | |||
772 | @Test | ||
773 | void alwaysFalseTest() { | ||
774 | var predicate = Query.of("AlwaysFalse", builder -> builder.parameter("p1")); | ||
775 | |||
776 | var store = ModelStore.builder() | ||
777 | .symbols(person) | ||
778 | .with(QueryInterpreterAdapter.builder() | ||
779 | .queries(predicate)) | ||
780 | .build(); | ||
781 | |||
782 | var model = store.createEmptyModel(); | ||
783 | var personInterpretation = model.getInterpretation(person); | ||
784 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
785 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
786 | |||
787 | personInterpretation.put(Tuple.of(0), true); | ||
788 | personInterpretation.put(Tuple.of(1), true); | ||
789 | personInterpretation.put(Tuple.of(2), true); | ||
790 | |||
791 | queryEngine.flushChanges(); | ||
792 | assertResults(Map.of(), predicateResultSet); | ||
793 | } | ||
794 | } | ||
diff --git a/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/QueryTransactionTest.java b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/QueryTransactionTest.java new file mode 100644 index 00000000..1cd05d91 --- /dev/null +++ b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/QueryTransactionTest.java | |||
@@ -0,0 +1,370 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.backend.QueryEvaluationHint; | ||
9 | import org.junit.jupiter.api.Test; | ||
10 | import tools.refinery.store.model.ModelStore; | ||
11 | import tools.refinery.store.query.ModelQueryAdapter; | ||
12 | import tools.refinery.store.query.dnf.Query; | ||
13 | import tools.refinery.store.query.dnf.RelationalQuery; | ||
14 | import tools.refinery.store.query.view.AnySymbolView; | ||
15 | import tools.refinery.store.query.view.FilteredView; | ||
16 | import tools.refinery.store.query.view.FunctionView; | ||
17 | import tools.refinery.store.query.view.KeyOnlyView; | ||
18 | import tools.refinery.store.representation.Symbol; | ||
19 | import tools.refinery.store.tuple.Tuple; | ||
20 | |||
21 | import java.util.Map; | ||
22 | import java.util.Optional; | ||
23 | |||
24 | import static org.junit.jupiter.api.Assertions.assertFalse; | ||
25 | import static org.junit.jupiter.api.Assertions.assertTrue; | ||
26 | import static tools.refinery.store.query.interpreter.tests.QueryAssertions.assertNullableResults; | ||
27 | import static tools.refinery.store.query.interpreter.tests.QueryAssertions.assertResults; | ||
28 | |||
29 | class QueryTransactionTest { | ||
30 | private static final Symbol<Boolean> person = Symbol.of("Person", 1); | ||
31 | private static final Symbol<Integer> age = Symbol.of("age", 1, Integer.class); | ||
32 | private static final AnySymbolView personView = new KeyOnlyView<>(person); | ||
33 | private static final AnySymbolView ageView = new FunctionView<>(age); | ||
34 | private static final RelationalQuery predicate = Query.of("TypeConstraint", (builder, p1) -> | ||
35 | builder.clause(personView.call(p1))); | ||
36 | |||
37 | @Test | ||
38 | void flushTest() { | ||
39 | var store = ModelStore.builder() | ||
40 | .symbols(person) | ||
41 | .with(QueryInterpreterAdapter.builder() | ||
42 | .queries(predicate)) | ||
43 | .build(); | ||
44 | |||
45 | var model = store.createEmptyModel(); | ||
46 | var personInterpretation = model.getInterpretation(person); | ||
47 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
48 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
49 | |||
50 | assertResults(Map.of( | ||
51 | Tuple.of(0), false, | ||
52 | Tuple.of(1), false, | ||
53 | Tuple.of(2), false, | ||
54 | Tuple.of(3), false | ||
55 | ), predicateResultSet); | ||
56 | assertFalse(queryEngine.hasPendingChanges()); | ||
57 | |||
58 | personInterpretation.put(Tuple.of(0), true); | ||
59 | personInterpretation.put(Tuple.of(1), true); | ||
60 | |||
61 | assertResults(Map.of( | ||
62 | Tuple.of(0), false, | ||
63 | Tuple.of(1), false, | ||
64 | Tuple.of(2), false, | ||
65 | Tuple.of(3), false | ||
66 | ), predicateResultSet); | ||
67 | assertTrue(queryEngine.hasPendingChanges()); | ||
68 | |||
69 | queryEngine.flushChanges(); | ||
70 | assertResults(Map.of( | ||
71 | Tuple.of(0), true, | ||
72 | Tuple.of(1), true, | ||
73 | Tuple.of(2), false, | ||
74 | Tuple.of(3), false | ||
75 | ), predicateResultSet); | ||
76 | assertFalse(queryEngine.hasPendingChanges()); | ||
77 | |||
78 | personInterpretation.put(Tuple.of(1), false); | ||
79 | personInterpretation.put(Tuple.of(2), true); | ||
80 | |||
81 | assertResults(Map.of( | ||
82 | Tuple.of(0), true, | ||
83 | Tuple.of(1), true, | ||
84 | Tuple.of(2), false, | ||
85 | Tuple.of(3), false | ||
86 | ), predicateResultSet); | ||
87 | assertTrue(queryEngine.hasPendingChanges()); | ||
88 | |||
89 | queryEngine.flushChanges(); | ||
90 | assertResults(Map.of( | ||
91 | Tuple.of(0), true, | ||
92 | Tuple.of(1), false, | ||
93 | Tuple.of(2), true, | ||
94 | Tuple.of(3), false | ||
95 | ), predicateResultSet); | ||
96 | assertFalse(queryEngine.hasPendingChanges()); | ||
97 | } | ||
98 | |||
99 | @Test | ||
100 | void localSearchTest() { | ||
101 | var store = ModelStore.builder() | ||
102 | .symbols(person) | ||
103 | .with(QueryInterpreterAdapter.builder() | ||
104 | .defaultHint(new QueryEvaluationHint(null, QueryEvaluationHint.BackendRequirement.DEFAULT_SEARCH)) | ||
105 | .queries(predicate)) | ||
106 | .build(); | ||
107 | |||
108 | var model = store.createEmptyModel(); | ||
109 | var personInterpretation = model.getInterpretation(person); | ||
110 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
111 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
112 | |||
113 | assertResults(Map.of( | ||
114 | Tuple.of(0), false, | ||
115 | Tuple.of(1), false, | ||
116 | Tuple.of(2), false, | ||
117 | Tuple.of(3), false | ||
118 | ), predicateResultSet); | ||
119 | assertFalse(queryEngine.hasPendingChanges()); | ||
120 | |||
121 | personInterpretation.put(Tuple.of(0), true); | ||
122 | personInterpretation.put(Tuple.of(1), true); | ||
123 | |||
124 | assertResults(Map.of( | ||
125 | Tuple.of(0), true, | ||
126 | Tuple.of(1), true, | ||
127 | Tuple.of(2), false, | ||
128 | Tuple.of(3), false | ||
129 | ), predicateResultSet); | ||
130 | assertFalse(queryEngine.hasPendingChanges()); | ||
131 | |||
132 | personInterpretation.put(Tuple.of(1), false); | ||
133 | personInterpretation.put(Tuple.of(2), true); | ||
134 | |||
135 | assertResults(Map.of( | ||
136 | Tuple.of(0), true, | ||
137 | Tuple.of(1), false, | ||
138 | Tuple.of(2), true, | ||
139 | Tuple.of(3), false | ||
140 | ), predicateResultSet); | ||
141 | assertFalse(queryEngine.hasPendingChanges()); | ||
142 | } | ||
143 | |||
144 | @Test | ||
145 | void unrelatedChangesTest() { | ||
146 | var asset = Symbol.of("Asset", 1); | ||
147 | |||
148 | var store = ModelStore.builder() | ||
149 | .symbols(person, asset) | ||
150 | .with(QueryInterpreterAdapter.builder() | ||
151 | .queries(predicate)) | ||
152 | .build(); | ||
153 | |||
154 | var model = store.createEmptyModel(); | ||
155 | var personInterpretation = model.getInterpretation(person); | ||
156 | var assetInterpretation = model.getInterpretation(asset); | ||
157 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
158 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
159 | |||
160 | assertFalse(queryEngine.hasPendingChanges()); | ||
161 | |||
162 | personInterpretation.put(Tuple.of(0), true); | ||
163 | personInterpretation.put(Tuple.of(1), true); | ||
164 | |||
165 | assetInterpretation.put(Tuple.of(1), true); | ||
166 | assetInterpretation.put(Tuple.of(2), true); | ||
167 | |||
168 | assertResults(Map.of( | ||
169 | Tuple.of(0), false, | ||
170 | Tuple.of(1), false, | ||
171 | Tuple.of(2), false, | ||
172 | Tuple.of(3), false, | ||
173 | Tuple.of(4), false | ||
174 | ), predicateResultSet); | ||
175 | assertTrue(queryEngine.hasPendingChanges()); | ||
176 | |||
177 | queryEngine.flushChanges(); | ||
178 | assertResults(Map.of( | ||
179 | Tuple.of(0), true, | ||
180 | Tuple.of(1), true, | ||
181 | Tuple.of(2), false, | ||
182 | Tuple.of(3), false, | ||
183 | Tuple.of(4), false | ||
184 | ), predicateResultSet); | ||
185 | assertFalse(queryEngine.hasPendingChanges()); | ||
186 | |||
187 | assetInterpretation.put(Tuple.of(3), true); | ||
188 | assertFalse(queryEngine.hasPendingChanges()); | ||
189 | |||
190 | assertResults(Map.of( | ||
191 | Tuple.of(0), true, | ||
192 | Tuple.of(1), true, | ||
193 | Tuple.of(2), false, | ||
194 | Tuple.of(3), false, | ||
195 | Tuple.of(4), false | ||
196 | ), predicateResultSet); | ||
197 | |||
198 | queryEngine.flushChanges(); | ||
199 | assertResults(Map.of( | ||
200 | Tuple.of(0), true, | ||
201 | Tuple.of(1), true, | ||
202 | Tuple.of(2), false, | ||
203 | Tuple.of(3), false, | ||
204 | Tuple.of(4), false | ||
205 | ), predicateResultSet); | ||
206 | assertFalse(queryEngine.hasPendingChanges()); | ||
207 | } | ||
208 | |||
209 | @Test | ||
210 | void tupleChangingChangeTest() { | ||
211 | var query = Query.of("TypeConstraint", Integer.class, (builder, p1, output) -> builder.clause( | ||
212 | personView.call(p1), | ||
213 | ageView.call(p1, output) | ||
214 | )); | ||
215 | |||
216 | var store = ModelStore.builder() | ||
217 | .symbols(person, age) | ||
218 | .with(QueryInterpreterAdapter.builder() | ||
219 | .queries(query)) | ||
220 | .build(); | ||
221 | |||
222 | var model = store.createEmptyModel(); | ||
223 | var personInterpretation = model.getInterpretation(person); | ||
224 | var ageInterpretation = model.getInterpretation(age); | ||
225 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
226 | var queryResultSet = queryEngine.getResultSet(query); | ||
227 | |||
228 | personInterpretation.put(Tuple.of(0), true); | ||
229 | |||
230 | ageInterpretation.put(Tuple.of(0), 24); | ||
231 | |||
232 | queryEngine.flushChanges(); | ||
233 | assertResults(Map.of(Tuple.of(0), 24), queryResultSet); | ||
234 | |||
235 | ageInterpretation.put(Tuple.of(0), 25); | ||
236 | |||
237 | queryEngine.flushChanges(); | ||
238 | assertResults(Map.of(Tuple.of(0), 25), queryResultSet); | ||
239 | |||
240 | ageInterpretation.put(Tuple.of(0), null); | ||
241 | |||
242 | queryEngine.flushChanges(); | ||
243 | assertNullableResults(Map.of(Tuple.of(0), Optional.empty()), queryResultSet); | ||
244 | } | ||
245 | |||
246 | @Test | ||
247 | void tuplePreservingUnchangedTest() { | ||
248 | var adultView = new FilteredView<>(age, "adult", n -> n != null && n >= 18); | ||
249 | |||
250 | var query = Query.of("TypeConstraint", (builder, p1) -> builder.clause( | ||
251 | personView.call(p1), | ||
252 | adultView.call(p1) | ||
253 | )); | ||
254 | |||
255 | var store = ModelStore.builder() | ||
256 | .symbols(person, age) | ||
257 | .with(QueryInterpreterAdapter.builder() | ||
258 | .queries(query)) | ||
259 | .build(); | ||
260 | |||
261 | var model = store.createEmptyModel(); | ||
262 | var personInterpretation = model.getInterpretation(person); | ||
263 | var ageInterpretation = model.getInterpretation(age); | ||
264 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
265 | var queryResultSet = queryEngine.getResultSet(query); | ||
266 | |||
267 | personInterpretation.put(Tuple.of(0), true); | ||
268 | |||
269 | ageInterpretation.put(Tuple.of(0), 24); | ||
270 | |||
271 | queryEngine.flushChanges(); | ||
272 | assertResults(Map.of(Tuple.of(0), true), queryResultSet); | ||
273 | |||
274 | ageInterpretation.put(Tuple.of(0), 25); | ||
275 | |||
276 | queryEngine.flushChanges(); | ||
277 | assertResults(Map.of(Tuple.of(0), true), queryResultSet); | ||
278 | |||
279 | ageInterpretation.put(Tuple.of(0), 17); | ||
280 | |||
281 | queryEngine.flushChanges(); | ||
282 | assertResults(Map.of(Tuple.of(0), false), queryResultSet); | ||
283 | } | ||
284 | |||
285 | @Test | ||
286 | void commitAfterFlushTest() { | ||
287 | var store = ModelStore.builder() | ||
288 | .symbols(person) | ||
289 | .with(QueryInterpreterAdapter.builder() | ||
290 | .queries(predicate)) | ||
291 | .build(); | ||
292 | |||
293 | var model = store.createEmptyModel(); | ||
294 | var personInterpretation = model.getInterpretation(person); | ||
295 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
296 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
297 | |||
298 | personInterpretation.put(Tuple.of(0), true); | ||
299 | personInterpretation.put(Tuple.of(1), true); | ||
300 | |||
301 | queryEngine.flushChanges(); | ||
302 | assertResults(Map.of( | ||
303 | Tuple.of(0), true, | ||
304 | Tuple.of(1), true, | ||
305 | Tuple.of(2), false, | ||
306 | Tuple.of(3), false | ||
307 | ), predicateResultSet); | ||
308 | |||
309 | var state1 = model.commit(); | ||
310 | |||
311 | personInterpretation.put(Tuple.of(1), false); | ||
312 | personInterpretation.put(Tuple.of(2), true); | ||
313 | |||
314 | queryEngine.flushChanges(); | ||
315 | assertResults(Map.of( | ||
316 | Tuple.of(0), true, | ||
317 | Tuple.of(1), false, | ||
318 | Tuple.of(2), true, | ||
319 | Tuple.of(3), false | ||
320 | ), predicateResultSet); | ||
321 | |||
322 | model.restore(state1); | ||
323 | |||
324 | assertFalse(queryEngine.hasPendingChanges()); | ||
325 | assertResults(Map.of( | ||
326 | Tuple.of(0), true, | ||
327 | Tuple.of(1), true, | ||
328 | Tuple.of(2), false, | ||
329 | Tuple.of(3), false | ||
330 | ), predicateResultSet); | ||
331 | } | ||
332 | |||
333 | @Test | ||
334 | void commitWithoutFlushTest() { | ||
335 | var store = ModelStore.builder() | ||
336 | .symbols(person) | ||
337 | .with(QueryInterpreterAdapter.builder() | ||
338 | .queries(predicate)) | ||
339 | .build(); | ||
340 | |||
341 | var model = store.createEmptyModel(); | ||
342 | var personInterpretation = model.getInterpretation(person); | ||
343 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
344 | var predicateResultSet = queryEngine.getResultSet(predicate); | ||
345 | |||
346 | personInterpretation.put(Tuple.of(0), true); | ||
347 | personInterpretation.put(Tuple.of(1), true); | ||
348 | |||
349 | assertResults(Map.of(), predicateResultSet); | ||
350 | assertTrue(queryEngine.hasPendingChanges()); | ||
351 | |||
352 | var state1 = model.commit(); | ||
353 | |||
354 | personInterpretation.put(Tuple.of(1), false); | ||
355 | personInterpretation.put(Tuple.of(2), true); | ||
356 | |||
357 | assertResults(Map.of(), predicateResultSet); | ||
358 | assertTrue(queryEngine.hasPendingChanges()); | ||
359 | |||
360 | model.restore(state1); | ||
361 | |||
362 | assertResults(Map.of( | ||
363 | Tuple.of(0), true, | ||
364 | Tuple.of(1), true, | ||
365 | Tuple.of(2), false, | ||
366 | Tuple.of(3), false | ||
367 | ), predicateResultSet); | ||
368 | assertFalse(queryEngine.hasPendingChanges()); | ||
369 | } | ||
370 | } | ||
diff --git a/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/StronglyConnectedComponentsTest.java b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/StronglyConnectedComponentsTest.java new file mode 100644 index 00000000..edbd9aff --- /dev/null +++ b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/StronglyConnectedComponentsTest.java | |||
@@ -0,0 +1,261 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter; | ||
7 | |||
8 | import org.junit.jupiter.api.Test; | ||
9 | import tools.refinery.store.model.ModelStore; | ||
10 | import tools.refinery.store.query.ModelQueryAdapter; | ||
11 | import tools.refinery.store.query.dnf.Query; | ||
12 | import tools.refinery.store.query.literal.Connectivity; | ||
13 | import tools.refinery.store.query.literal.RepresentativeElectionLiteral; | ||
14 | import tools.refinery.store.query.view.AnySymbolView; | ||
15 | import tools.refinery.store.query.view.KeyOnlyView; | ||
16 | import tools.refinery.store.representation.Symbol; | ||
17 | import tools.refinery.store.tuple.Tuple; | ||
18 | |||
19 | import java.util.List; | ||
20 | import java.util.Map; | ||
21 | |||
22 | import static org.hamcrest.MatcherAssert.assertThat; | ||
23 | import static org.hamcrest.Matchers.is; | ||
24 | import static tools.refinery.store.query.interpreter.tests.QueryAssertions.assertResults; | ||
25 | |||
26 | class StronglyConnectedComponentsTest { | ||
27 | private static final Symbol<Boolean> friend = Symbol.of("friend", 2); | ||
28 | private static final AnySymbolView friendView = new KeyOnlyView<>(friend); | ||
29 | |||
30 | @Test | ||
31 | void symbolViewTest() { | ||
32 | var query = Query.of("SymbolViewRepresentative", (builder, p1, p2) -> builder | ||
33 | .clause(v1 -> List.of( | ||
34 | new RepresentativeElectionLiteral(Connectivity.STRONG, friendView, p1, v1), | ||
35 | new RepresentativeElectionLiteral(Connectivity.STRONG, friendView, p2, v1) | ||
36 | ))); | ||
37 | |||
38 | var store = ModelStore.builder() | ||
39 | .symbols(friend) | ||
40 | .with(QueryInterpreterAdapter.builder() | ||
41 | .queries(query)) | ||
42 | .build(); | ||
43 | |||
44 | var model = store.createEmptyModel(); | ||
45 | var friendInterpretation = model.getInterpretation(friend); | ||
46 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
47 | var resultSet = queryEngine.getResultSet(query); | ||
48 | |||
49 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
50 | friendInterpretation.put(Tuple.of(1, 0), true); | ||
51 | friendInterpretation.put(Tuple.of(1, 2), true); | ||
52 | queryEngine.flushChanges(); | ||
53 | |||
54 | assertResults(Map.of( | ||
55 | Tuple.of(0, 0), true, | ||
56 | Tuple.of(0, 1), true, | ||
57 | Tuple.of(1, 0), true, | ||
58 | Tuple.of(1, 1), true, | ||
59 | Tuple.of(2, 2), true | ||
60 | ), resultSet); | ||
61 | } | ||
62 | |||
63 | @Test | ||
64 | void symbolViewInsertTest() { | ||
65 | var query = Query.of("SymbolViewRepresentative", (builder, p1, p2) -> builder | ||
66 | .clause(v1 -> List.of( | ||
67 | new RepresentativeElectionLiteral(Connectivity.STRONG, friendView, p1, v1), | ||
68 | new RepresentativeElectionLiteral(Connectivity.STRONG, friendView, p2, v1) | ||
69 | ))); | ||
70 | |||
71 | var store = ModelStore.builder() | ||
72 | .symbols(friend) | ||
73 | .with(QueryInterpreterAdapter.builder() | ||
74 | .queries(query)) | ||
75 | .build(); | ||
76 | |||
77 | var model = store.createEmptyModel(); | ||
78 | var friendInterpretation = model.getInterpretation(friend); | ||
79 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
80 | var resultSet = queryEngine.getResultSet(query); | ||
81 | |||
82 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
83 | friendInterpretation.put(Tuple.of(1, 0), true); | ||
84 | friendInterpretation.put(Tuple.of(1, 2), true); | ||
85 | queryEngine.flushChanges(); | ||
86 | |||
87 | friendInterpretation.put(Tuple.of(2, 0), true); | ||
88 | friendInterpretation.put(Tuple.of(0, 3), true); | ||
89 | queryEngine.flushChanges(); | ||
90 | |||
91 | assertResults(Map.of( | ||
92 | Tuple.of(0, 0), true, | ||
93 | Tuple.of(0, 1), true, | ||
94 | Tuple.of(0, 2), true, | ||
95 | Tuple.of(1, 1), true, | ||
96 | Tuple.of(1, 0), true, | ||
97 | Tuple.of(1, 2), true, | ||
98 | Tuple.of(2, 0), true, | ||
99 | Tuple.of(2, 1), true, | ||
100 | Tuple.of(2, 2), true, | ||
101 | Tuple.of(3, 3), true | ||
102 | ), resultSet); | ||
103 | } | ||
104 | |||
105 | @Test | ||
106 | void symbolViewDeleteTest() { | ||
107 | var query = Query.of("SymbolViewRepresentative", (builder, p1, p2) -> builder | ||
108 | .clause(v1 -> List.of( | ||
109 | new RepresentativeElectionLiteral(Connectivity.STRONG, friendView, p1, v1), | ||
110 | new RepresentativeElectionLiteral(Connectivity.STRONG, friendView, p2, v1) | ||
111 | ))); | ||
112 | |||
113 | var store = ModelStore.builder() | ||
114 | .symbols(friend) | ||
115 | .with(QueryInterpreterAdapter.builder() | ||
116 | .queries(query)) | ||
117 | .build(); | ||
118 | |||
119 | var model = store.createEmptyModel(); | ||
120 | var friendInterpretation = model.getInterpretation(friend); | ||
121 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
122 | var resultSet = queryEngine.getResultSet(query); | ||
123 | |||
124 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
125 | friendInterpretation.put(Tuple.of(1, 0), true); | ||
126 | friendInterpretation.put(Tuple.of(1, 2), true); | ||
127 | queryEngine.flushChanges(); | ||
128 | |||
129 | friendInterpretation.put(Tuple.of(1, 0), false); | ||
130 | friendInterpretation.put(Tuple.of(1, 2), false); | ||
131 | queryEngine.flushChanges(); | ||
132 | |||
133 | assertResults(Map.of( | ||
134 | Tuple.of(0, 0), true, | ||
135 | Tuple.of(1, 1), true | ||
136 | ), resultSet); | ||
137 | } | ||
138 | |||
139 | @Test | ||
140 | void diagonalSymbolViewTest() { | ||
141 | var person = Symbol.of("Person", 1); | ||
142 | var personView = new KeyOnlyView<>(person); | ||
143 | |||
144 | var query = Query.of("SymbolViewRepresentative", (builder, p1) -> builder | ||
145 | .clause( | ||
146 | personView.call(p1), | ||
147 | new RepresentativeElectionLiteral(Connectivity.STRONG, friendView, p1, p1) | ||
148 | )); | ||
149 | |||
150 | var store = ModelStore.builder() | ||
151 | .symbols(person, friend) | ||
152 | .with(QueryInterpreterAdapter.builder() | ||
153 | .queries(query)) | ||
154 | .build(); | ||
155 | |||
156 | var model = store.createEmptyModel(); | ||
157 | var personInterpretation = model.getInterpretation(person); | ||
158 | var friendInterpretation = model.getInterpretation(friend); | ||
159 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
160 | var resultSet = queryEngine.getResultSet(query); | ||
161 | |||
162 | personInterpretation.put(Tuple.of(0), true); | ||
163 | personInterpretation.put(Tuple.of(1), true); | ||
164 | personInterpretation.put(Tuple.of(2), true); | ||
165 | |||
166 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
167 | friendInterpretation.put(Tuple.of(1, 0), true); | ||
168 | friendInterpretation.put(Tuple.of(1, 2), true); | ||
169 | queryEngine.flushChanges(); | ||
170 | |||
171 | assertThat(resultSet.size(), is(2)); | ||
172 | assertThat(resultSet.get(Tuple.of(2)), is(true)); | ||
173 | } | ||
174 | |||
175 | @Test | ||
176 | void diagonalDnfTest() { | ||
177 | var person = Symbol.of("Person", 1); | ||
178 | var personView = new KeyOnlyView<>(person); | ||
179 | |||
180 | var subQuery = Query.of("SubQuery", (builder, p1, p2) -> builder | ||
181 | .clause( | ||
182 | personView.call(p1), | ||
183 | personView.call(p2), | ||
184 | friendView.call(p1, p2) | ||
185 | )) | ||
186 | .getDnf(); | ||
187 | var query = Query.of("SymbolViewRepresentative", (builder, p1) -> builder | ||
188 | .clause( | ||
189 | personView.call(p1), | ||
190 | new RepresentativeElectionLiteral(Connectivity.STRONG, subQuery, p1, p1) | ||
191 | )); | ||
192 | |||
193 | var store = ModelStore.builder() | ||
194 | .symbols(person, friend) | ||
195 | .with(QueryInterpreterAdapter.builder() | ||
196 | .queries(query)) | ||
197 | .build(); | ||
198 | |||
199 | var model = store.createEmptyModel(); | ||
200 | var personInterpretation = model.getInterpretation(person); | ||
201 | var friendInterpretation = model.getInterpretation(friend); | ||
202 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
203 | var resultSet = queryEngine.getResultSet(query); | ||
204 | |||
205 | personInterpretation.put(Tuple.of(0), true); | ||
206 | personInterpretation.put(Tuple.of(1), true); | ||
207 | personInterpretation.put(Tuple.of(2), true); | ||
208 | |||
209 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
210 | friendInterpretation.put(Tuple.of(1, 0), true); | ||
211 | friendInterpretation.put(Tuple.of(1, 2), true); | ||
212 | queryEngine.flushChanges(); | ||
213 | |||
214 | assertThat(resultSet.size(), is(2)); | ||
215 | assertThat(resultSet.get(Tuple.of(2)), is(true)); | ||
216 | } | ||
217 | |||
218 | @Test | ||
219 | void loopTest() { | ||
220 | var query = Query.of("SymbolViewRepresentative", (builder, p1, p2) -> builder | ||
221 | .clause(v1 -> List.of( | ||
222 | new RepresentativeElectionLiteral(Connectivity.STRONG, friendView, p1, v1), | ||
223 | new RepresentativeElectionLiteral(Connectivity.STRONG, friendView, p2, v1) | ||
224 | ))); | ||
225 | |||
226 | var store = ModelStore.builder() | ||
227 | .symbols(friend) | ||
228 | .with(QueryInterpreterAdapter.builder() | ||
229 | .queries(query)) | ||
230 | .build(); | ||
231 | |||
232 | var model = store.createEmptyModel(); | ||
233 | var friendInterpretation = model.getInterpretation(friend); | ||
234 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
235 | var resultSet = queryEngine.getResultSet(query); | ||
236 | |||
237 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
238 | friendInterpretation.put(Tuple.of(1, 2), true); | ||
239 | friendInterpretation.put(Tuple.of(2, 3), true); | ||
240 | friendInterpretation.put(Tuple.of(3, 0), true); | ||
241 | friendInterpretation.put(Tuple.of(3, 4), true); | ||
242 | queryEngine.flushChanges(); | ||
243 | |||
244 | assertThat(resultSet.get(Tuple.of(0, 1)), is(true)); | ||
245 | assertThat(resultSet.get(Tuple.of(1, 2)), is(true)); | ||
246 | assertThat(resultSet.get(Tuple.of(2, 3)), is(true)); | ||
247 | assertThat(resultSet.get(Tuple.of(3, 0)), is(true)); | ||
248 | assertThat(resultSet.get(Tuple.of(3, 4)), is(false)); | ||
249 | |||
250 | friendInterpretation.put(Tuple.of(2, 3), false); | ||
251 | queryEngine.flushChanges(); | ||
252 | |||
253 | assertThat(resultSet.get(Tuple.of(0, 1)), is(false)); | ||
254 | assertThat(resultSet.get(Tuple.of(0, 2)), is(false)); | ||
255 | assertThat(resultSet.get(Tuple.of(0, 3)), is(false)); | ||
256 | assertThat(resultSet.get(Tuple.of(1, 2)), is(false)); | ||
257 | assertThat(resultSet.get(Tuple.of(2, 3)), is(false)); | ||
258 | assertThat(resultSet.get(Tuple.of(3, 0)), is(false)); | ||
259 | assertThat(resultSet.get(Tuple.of(3, 4)), is(false)); | ||
260 | } | ||
261 | } | ||
diff --git a/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/WeaklyConnectedComponentsTest.java b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/WeaklyConnectedComponentsTest.java new file mode 100644 index 00000000..3fc85480 --- /dev/null +++ b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/WeaklyConnectedComponentsTest.java | |||
@@ -0,0 +1,188 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter; | ||
7 | |||
8 | import org.junit.jupiter.api.Test; | ||
9 | import tools.refinery.store.model.ModelStore; | ||
10 | import tools.refinery.store.query.ModelQueryAdapter; | ||
11 | import tools.refinery.store.query.dnf.Query; | ||
12 | import tools.refinery.store.query.literal.Connectivity; | ||
13 | import tools.refinery.store.query.literal.RepresentativeElectionLiteral; | ||
14 | import tools.refinery.store.query.view.AnySymbolView; | ||
15 | import tools.refinery.store.query.view.KeyOnlyView; | ||
16 | import tools.refinery.store.representation.Symbol; | ||
17 | import tools.refinery.store.tuple.Tuple; | ||
18 | |||
19 | import java.util.List; | ||
20 | import java.util.Map; | ||
21 | |||
22 | import static org.hamcrest.MatcherAssert.assertThat; | ||
23 | import static org.hamcrest.Matchers.is; | ||
24 | import static tools.refinery.store.query.interpreter.tests.QueryAssertions.assertResults; | ||
25 | |||
26 | class WeaklyConnectedComponentsTest { | ||
27 | private static final Symbol<Boolean> friend = Symbol.of("friend", 2); | ||
28 | private static final AnySymbolView friendView = new KeyOnlyView<>(friend); | ||
29 | |||
30 | @Test | ||
31 | void symbolViewTest() { | ||
32 | var query = Query.of("SymbolViewRepresentative", (builder, p1, p2) -> builder | ||
33 | .clause(v1 -> List.of( | ||
34 | new RepresentativeElectionLiteral(Connectivity.WEAK, friendView, p1, v1), | ||
35 | new RepresentativeElectionLiteral(Connectivity.WEAK, friendView, p2, v1) | ||
36 | ))); | ||
37 | |||
38 | var store = ModelStore.builder() | ||
39 | .symbols(friend) | ||
40 | .with(QueryInterpreterAdapter.builder() | ||
41 | .queries(query)) | ||
42 | .build(); | ||
43 | |||
44 | var model = store.createEmptyModel(); | ||
45 | var friendInterpretation = model.getInterpretation(friend); | ||
46 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
47 | var resultSet = queryEngine.getResultSet(query); | ||
48 | |||
49 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
50 | friendInterpretation.put(Tuple.of(1, 0), true); | ||
51 | friendInterpretation.put(Tuple.of(2, 3), true); | ||
52 | queryEngine.flushChanges(); | ||
53 | |||
54 | assertResults(Map.of( | ||
55 | Tuple.of(0, 0), true, | ||
56 | Tuple.of(0, 1), true, | ||
57 | Tuple.of(1, 0), true, | ||
58 | Tuple.of(1, 1), true, | ||
59 | Tuple.of(2, 2), true, | ||
60 | Tuple.of(2, 3), true, | ||
61 | Tuple.of(3, 2), true, | ||
62 | Tuple.of(3, 3), true | ||
63 | ), resultSet); | ||
64 | } | ||
65 | |||
66 | @Test | ||
67 | void symbolViewUpdateTest() { | ||
68 | var query = Query.of("SymbolViewRepresentative", (builder, p1, p2) -> builder | ||
69 | .clause(v1 -> List.of( | ||
70 | new RepresentativeElectionLiteral(Connectivity.WEAK, friendView, p1, v1), | ||
71 | new RepresentativeElectionLiteral(Connectivity.WEAK, friendView, p2, v1) | ||
72 | ))); | ||
73 | |||
74 | var store = ModelStore.builder() | ||
75 | .symbols(friend) | ||
76 | .with(QueryInterpreterAdapter.builder() | ||
77 | .queries(query)) | ||
78 | .build(); | ||
79 | |||
80 | var model = store.createEmptyModel(); | ||
81 | var friendInterpretation = model.getInterpretation(friend); | ||
82 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
83 | var resultSet = queryEngine.getResultSet(query); | ||
84 | |||
85 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
86 | friendInterpretation.put(Tuple.of(1, 0), true); | ||
87 | friendInterpretation.put(Tuple.of(2, 3), true); | ||
88 | queryEngine.flushChanges(); | ||
89 | |||
90 | friendInterpretation.put(Tuple.of(2, 3), false); | ||
91 | friendInterpretation.put(Tuple.of(1, 0), false); | ||
92 | friendInterpretation.put(Tuple.of(1, 2), true); | ||
93 | queryEngine.flushChanges(); | ||
94 | |||
95 | assertResults(Map.of( | ||
96 | Tuple.of(0, 0), true, | ||
97 | Tuple.of(0, 1), true, | ||
98 | Tuple.of(0, 2), true, | ||
99 | Tuple.of(1, 0), true, | ||
100 | Tuple.of(1, 1), true, | ||
101 | Tuple.of(1, 2), true, | ||
102 | Tuple.of(2, 0), true, | ||
103 | Tuple.of(2, 1), true, | ||
104 | Tuple.of(2, 2), true | ||
105 | ), resultSet); | ||
106 | } | ||
107 | |||
108 | @Test | ||
109 | void diagonalSymbolViewTest() { | ||
110 | var person = Symbol.of("Person", 1); | ||
111 | var personView = new KeyOnlyView<>(person); | ||
112 | |||
113 | var query = Query.of("SymbolViewRepresentative", (builder, p1) -> builder | ||
114 | .clause( | ||
115 | personView.call(p1), | ||
116 | new RepresentativeElectionLiteral(Connectivity.WEAK, friendView, p1, p1) | ||
117 | )); | ||
118 | |||
119 | var store = ModelStore.builder() | ||
120 | .symbols(person, friend) | ||
121 | .with(QueryInterpreterAdapter.builder() | ||
122 | .queries(query)) | ||
123 | .build(); | ||
124 | |||
125 | var model = store.createEmptyModel(); | ||
126 | var personInterpretation = model.getInterpretation(person); | ||
127 | var friendInterpretation = model.getInterpretation(friend); | ||
128 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
129 | var resultSet = queryEngine.getResultSet(query); | ||
130 | |||
131 | personInterpretation.put(Tuple.of(0), true); | ||
132 | personInterpretation.put(Tuple.of(1), true); | ||
133 | personInterpretation.put(Tuple.of(2), true); | ||
134 | personInterpretation.put(Tuple.of(3), true); | ||
135 | |||
136 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
137 | friendInterpretation.put(Tuple.of(1, 0), true); | ||
138 | friendInterpretation.put(Tuple.of(2, 3), true); | ||
139 | queryEngine.flushChanges(); | ||
140 | |||
141 | assertThat(resultSet.size(), is(2)); | ||
142 | assertThat(resultSet.get(Tuple.of(2)), is(true)); | ||
143 | } | ||
144 | |||
145 | @Test | ||
146 | void diagonalDnfTest() { | ||
147 | var person = Symbol.of("Person", 1); | ||
148 | var personView = new KeyOnlyView<>(person); | ||
149 | |||
150 | var subQuery = Query.of("SubQuery", (builder, p1, p2) -> builder | ||
151 | .clause( | ||
152 | personView.call(p1), | ||
153 | personView.call(p2), | ||
154 | friendView.call(p1, p2) | ||
155 | )) | ||
156 | .getDnf(); | ||
157 | var query = Query.of("SymbolViewRepresentative", (builder, p1) -> builder | ||
158 | .clause( | ||
159 | personView.call(p1), | ||
160 | new RepresentativeElectionLiteral(Connectivity.WEAK, subQuery, p1, p1) | ||
161 | )); | ||
162 | |||
163 | var store = ModelStore.builder() | ||
164 | .symbols(person, friend) | ||
165 | .with(QueryInterpreterAdapter.builder() | ||
166 | .queries(query)) | ||
167 | .build(); | ||
168 | |||
169 | var model = store.createEmptyModel(); | ||
170 | var personInterpretation = model.getInterpretation(person); | ||
171 | var friendInterpretation = model.getInterpretation(friend); | ||
172 | var queryEngine = model.getAdapter(ModelQueryAdapter.class); | ||
173 | var resultSet = queryEngine.getResultSet(query); | ||
174 | |||
175 | personInterpretation.put(Tuple.of(0), true); | ||
176 | personInterpretation.put(Tuple.of(1), true); | ||
177 | personInterpretation.put(Tuple.of(2), true); | ||
178 | personInterpretation.put(Tuple.of(3), true); | ||
179 | |||
180 | friendInterpretation.put(Tuple.of(0, 1), true); | ||
181 | friendInterpretation.put(Tuple.of(1, 0), true); | ||
182 | friendInterpretation.put(Tuple.of(2, 3), true); | ||
183 | queryEngine.flushChanges(); | ||
184 | |||
185 | assertThat(resultSet.size(), is(2)); | ||
186 | assertThat(resultSet.get(Tuple.of(2)), is(true)); | ||
187 | } | ||
188 | } | ||
diff --git a/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/internal/matcher/MatcherUtilsTest.java b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/internal/matcher/MatcherUtilsTest.java new file mode 100644 index 00000000..1c8044ea --- /dev/null +++ b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/internal/matcher/MatcherUtilsTest.java | |||
@@ -0,0 +1,239 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.internal.matcher; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.tuple.*; | ||
9 | import org.junit.jupiter.api.Test; | ||
10 | import tools.refinery.store.tuple.Tuple; | ||
11 | import tools.refinery.store.tuple.*; | ||
12 | |||
13 | import java.util.List; | ||
14 | |||
15 | import static org.hamcrest.MatcherAssert.assertThat; | ||
16 | import static org.hamcrest.Matchers.*; | ||
17 | import static org.junit.jupiter.api.Assertions.assertThrows; | ||
18 | |||
19 | class MatcherUtilsTest { | ||
20 | @Test | ||
21 | void toViatra0Test() { | ||
22 | var viatraTuple = MatcherUtils.toViatraTuple(Tuple.of()); | ||
23 | assertThat(viatraTuple.getSize(), is(0)); | ||
24 | assertThat(viatraTuple, instanceOf(FlatTuple0.class)); | ||
25 | } | ||
26 | |||
27 | @Test | ||
28 | void toViatra1Test() { | ||
29 | var viatraTuple = MatcherUtils.toViatraTuple(Tuple.of(2)); | ||
30 | assertThat(viatraTuple.getSize(), is(1)); | ||
31 | assertThat(viatraTuple.get(0), is(Tuple.of(2))); | ||
32 | assertThat(viatraTuple, instanceOf(FlatTuple1.class)); | ||
33 | } | ||
34 | |||
35 | @Test | ||
36 | void toViatra2Test() { | ||
37 | var viatraTuple = MatcherUtils.toViatraTuple(Tuple.of(2, 3)); | ||
38 | assertThat(viatraTuple.getSize(), is(2)); | ||
39 | assertThat(viatraTuple.get(0), is(Tuple.of(2))); | ||
40 | assertThat(viatraTuple.get(1), is(Tuple.of(3))); | ||
41 | assertThat(viatraTuple, instanceOf(FlatTuple2.class)); | ||
42 | } | ||
43 | |||
44 | @Test | ||
45 | void toViatra3Test() { | ||
46 | var viatraTuple = MatcherUtils.toViatraTuple(Tuple.of(2, 3, 5)); | ||
47 | assertThat(viatraTuple.getSize(), is(3)); | ||
48 | assertThat(viatraTuple.get(0), is(Tuple.of(2))); | ||
49 | assertThat(viatraTuple.get(1), is(Tuple.of(3))); | ||
50 | assertThat(viatraTuple.get(2), is(Tuple.of(5))); | ||
51 | assertThat(viatraTuple, instanceOf(FlatTuple3.class)); | ||
52 | } | ||
53 | |||
54 | @Test | ||
55 | void toViatra4Test() { | ||
56 | var viatraTuple = MatcherUtils.toViatraTuple(Tuple.of(2, 3, 5, 8)); | ||
57 | assertThat(viatraTuple.getSize(), is(4)); | ||
58 | assertThat(viatraTuple.get(0), is(Tuple.of(2))); | ||
59 | assertThat(viatraTuple.get(1), is(Tuple.of(3))); | ||
60 | assertThat(viatraTuple.get(2), is(Tuple.of(5))); | ||
61 | assertThat(viatraTuple.get(3), is(Tuple.of(8))); | ||
62 | assertThat(viatraTuple, instanceOf(FlatTuple4.class)); | ||
63 | } | ||
64 | |||
65 | @Test | ||
66 | void toViatra5Test() { | ||
67 | var viatraTuple = MatcherUtils.toViatraTuple(Tuple.of(2, 3, 5, 8, 13)); | ||
68 | assertThat(viatraTuple.getSize(), is(5)); | ||
69 | assertThat(viatraTuple.get(0), is(Tuple.of(2))); | ||
70 | assertThat(viatraTuple.get(1), is(Tuple.of(3))); | ||
71 | assertThat(viatraTuple.get(2), is(Tuple.of(5))); | ||
72 | assertThat(viatraTuple.get(3), is(Tuple.of(8))); | ||
73 | assertThat(viatraTuple.get(4), is(Tuple.of(13))); | ||
74 | assertThat(viatraTuple, instanceOf(FlatTuple.class)); | ||
75 | } | ||
76 | |||
77 | @Test | ||
78 | void toRefinery0Test() { | ||
79 | var refineryTuple = MatcherUtils.toRefineryTuple(Tuples.flatTupleOf()); | ||
80 | assertThat(refineryTuple.getSize(), is(0)); | ||
81 | assertThat(refineryTuple, instanceOf(Tuple0.class)); | ||
82 | } | ||
83 | |||
84 | @Test | ||
85 | void toRefinery1Test() { | ||
86 | var refineryTuple = MatcherUtils.toRefineryTuple(Tuples.flatTupleOf(Tuple.of(2))); | ||
87 | assertThat(refineryTuple.getSize(), is(1)); | ||
88 | assertThat(refineryTuple.get(0), is(2)); | ||
89 | assertThat(refineryTuple, instanceOf(Tuple1.class)); | ||
90 | } | ||
91 | |||
92 | @Test | ||
93 | void toRefinery2Test() { | ||
94 | var refineryTuple = MatcherUtils.toRefineryTuple(Tuples.flatTupleOf(Tuple.of(2), Tuple.of(3))); | ||
95 | assertThat(refineryTuple.getSize(), is(2)); | ||
96 | assertThat(refineryTuple.get(0), is(2)); | ||
97 | assertThat(refineryTuple.get(1), is(3)); | ||
98 | assertThat(refineryTuple, instanceOf(Tuple2.class)); | ||
99 | } | ||
100 | |||
101 | @Test | ||
102 | void toRefinery3Test() { | ||
103 | var refineryTuple = MatcherUtils.toRefineryTuple(Tuples.flatTupleOf(Tuple.of(2), Tuple.of(3), Tuple.of(5))); | ||
104 | assertThat(refineryTuple.getSize(), is(3)); | ||
105 | assertThat(refineryTuple.get(0), is(2)); | ||
106 | assertThat(refineryTuple.get(1), is(3)); | ||
107 | assertThat(refineryTuple.get(2), is(5)); | ||
108 | assertThat(refineryTuple, instanceOf(Tuple3.class)); | ||
109 | } | ||
110 | |||
111 | @Test | ||
112 | void toRefinery4Test() { | ||
113 | var refineryTuple = MatcherUtils.toRefineryTuple(Tuples.flatTupleOf(Tuple.of(2), Tuple.of(3), Tuple.of(5), | ||
114 | Tuple.of(8))); | ||
115 | assertThat(refineryTuple.getSize(), is(4)); | ||
116 | assertThat(refineryTuple.get(0), is(2)); | ||
117 | assertThat(refineryTuple.get(1), is(3)); | ||
118 | assertThat(refineryTuple.get(2), is(5)); | ||
119 | assertThat(refineryTuple.get(3), is(8)); | ||
120 | assertThat(refineryTuple, instanceOf(Tuple4.class)); | ||
121 | } | ||
122 | |||
123 | @Test | ||
124 | void toRefinery5Test() { | ||
125 | var refineryTuple = MatcherUtils.toRefineryTuple(Tuples.flatTupleOf(Tuple.of(2), Tuple.of(3), Tuple.of(5), | ||
126 | Tuple.of(8), Tuple.of(13))); | ||
127 | assertThat(refineryTuple.getSize(), is(5)); | ||
128 | assertThat(refineryTuple.get(0), is(2)); | ||
129 | assertThat(refineryTuple.get(1), is(3)); | ||
130 | assertThat(refineryTuple.get(2), is(5)); | ||
131 | assertThat(refineryTuple.get(3), is(8)); | ||
132 | assertThat(refineryTuple.get(4), is(13)); | ||
133 | assertThat(refineryTuple, instanceOf(TupleN.class)); | ||
134 | } | ||
135 | |||
136 | @Test | ||
137 | void toRefineryInvalidValueTest() { | ||
138 | var viatraTuple = Tuples.flatTupleOf(Tuple.of(2), -98); | ||
139 | assertThrows(IllegalArgumentException.class, () -> MatcherUtils.toRefineryTuple(viatraTuple)); | ||
140 | } | ||
141 | |||
142 | @Test | ||
143 | void keyToRefinery0Test() { | ||
144 | var refineryTuple = MatcherUtils.keyToRefineryTuple(Tuples.flatTupleOf(-99)); | ||
145 | assertThat(refineryTuple.getSize(), is(0)); | ||
146 | assertThat(refineryTuple, instanceOf(Tuple0.class)); | ||
147 | } | ||
148 | |||
149 | @Test | ||
150 | void keyToRefinery1Test() { | ||
151 | var refineryTuple = MatcherUtils.keyToRefineryTuple(Tuples.flatTupleOf(Tuple.of(2), -99)); | ||
152 | assertThat(refineryTuple.getSize(), is(1)); | ||
153 | assertThat(refineryTuple.get(0), is(2)); | ||
154 | assertThat(refineryTuple, instanceOf(Tuple1.class)); | ||
155 | } | ||
156 | |||
157 | @Test | ||
158 | void keyToRefinery2Test() { | ||
159 | var refineryTuple = MatcherUtils.keyToRefineryTuple(Tuples.flatTupleOf(Tuple.of(2), Tuple.of(3), -99)); | ||
160 | assertThat(refineryTuple.getSize(), is(2)); | ||
161 | assertThat(refineryTuple.get(0), is(2)); | ||
162 | assertThat(refineryTuple.get(1), is(3)); | ||
163 | assertThat(refineryTuple, instanceOf(Tuple2.class)); | ||
164 | } | ||
165 | |||
166 | @Test | ||
167 | void keyToRefinery3Test() { | ||
168 | var refineryTuple = MatcherUtils.keyToRefineryTuple(Tuples.flatTupleOf(Tuple.of(2), Tuple.of(3), Tuple.of(5), | ||
169 | -99)); | ||
170 | assertThat(refineryTuple.getSize(), is(3)); | ||
171 | assertThat(refineryTuple.get(0), is(2)); | ||
172 | assertThat(refineryTuple.get(1), is(3)); | ||
173 | assertThat(refineryTuple.get(2), is(5)); | ||
174 | assertThat(refineryTuple, instanceOf(Tuple3.class)); | ||
175 | } | ||
176 | |||
177 | @Test | ||
178 | void keyToRefinery4Test() { | ||
179 | var refineryTuple = MatcherUtils.keyToRefineryTuple(Tuples.flatTupleOf(Tuple.of(2), Tuple.of(3), Tuple.of(5), | ||
180 | Tuple.of(8), -99)); | ||
181 | assertThat(refineryTuple.getSize(), is(4)); | ||
182 | assertThat(refineryTuple.get(0), is(2)); | ||
183 | assertThat(refineryTuple.get(1), is(3)); | ||
184 | assertThat(refineryTuple.get(2), is(5)); | ||
185 | assertThat(refineryTuple.get(3), is(8)); | ||
186 | assertThat(refineryTuple, instanceOf(Tuple4.class)); | ||
187 | } | ||
188 | |||
189 | @Test | ||
190 | void keyToRefinery5Test() { | ||
191 | var refineryTuple = MatcherUtils.keyToRefineryTuple(Tuples.flatTupleOf(Tuple.of(2), Tuple.of(3), Tuple.of(5), | ||
192 | Tuple.of(8), Tuple.of(13), -99)); | ||
193 | assertThat(refineryTuple.getSize(), is(5)); | ||
194 | assertThat(refineryTuple.get(0), is(2)); | ||
195 | assertThat(refineryTuple.get(1), is(3)); | ||
196 | assertThat(refineryTuple.get(2), is(5)); | ||
197 | assertThat(refineryTuple.get(3), is(8)); | ||
198 | assertThat(refineryTuple.get(4), is(13)); | ||
199 | assertThat(refineryTuple, instanceOf(TupleN.class)); | ||
200 | } | ||
201 | |||
202 | @Test | ||
203 | void keyToRefineryTooShortTest() { | ||
204 | var viatraTuple = Tuples.flatTupleOf(); | ||
205 | assertThrows(IllegalArgumentException.class, () -> MatcherUtils.keyToRefineryTuple(viatraTuple)); | ||
206 | } | ||
207 | |||
208 | @Test | ||
209 | void keyToRefineryInvalidValueTest() { | ||
210 | var viatraTuple = Tuples.flatTupleOf(Tuple.of(2), -98, -99); | ||
211 | assertThrows(IllegalArgumentException.class, () -> MatcherUtils.keyToRefineryTuple(viatraTuple)); | ||
212 | } | ||
213 | |||
214 | @Test | ||
215 | void getSingleValueTest() { | ||
216 | var value = MatcherUtils.getSingleValue(List.of(Tuples.flatTupleOf(Tuple.of(2), -99))); | ||
217 | assertThat(value, is(-99)); | ||
218 | } | ||
219 | |||
220 | // Static analysis accurately determines that the result is always {@code null}, but we check anyways. | ||
221 | @SuppressWarnings("ConstantValue") | ||
222 | @Test | ||
223 | void getSingleValueNullTest() { | ||
224 | var value = MatcherUtils.getSingleValue((Iterable<? extends ITuple>) null); | ||
225 | assertThat(value, nullValue()); | ||
226 | } | ||
227 | |||
228 | @Test | ||
229 | void getSingleValueEmptyTest() { | ||
230 | var value = MatcherUtils.getSingleValue(List.of()); | ||
231 | assertThat(value, nullValue()); | ||
232 | } | ||
233 | |||
234 | @Test | ||
235 | void getSingleValueMultipleTest() { | ||
236 | var viatraTuples = List.of(Tuples.flatTupleOf(Tuple.of(2), -98), Tuples.flatTupleOf(Tuple.of(2), -99)); | ||
237 | assertThrows(IllegalStateException.class, () -> MatcherUtils.getSingleValue(viatraTuples)); | ||
238 | } | ||
239 | } | ||
diff --git a/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/tests/QueryAssertions.java b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/tests/QueryAssertions.java new file mode 100644 index 00000000..c4659a98 --- /dev/null +++ b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/tests/QueryAssertions.java | |||
@@ -0,0 +1,57 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.tests; | ||
7 | |||
8 | import org.junit.jupiter.api.function.Executable; | ||
9 | import tools.refinery.store.query.resultset.ResultSet; | ||
10 | import tools.refinery.store.tuple.Tuple; | ||
11 | |||
12 | import java.util.*; | ||
13 | |||
14 | import static org.hamcrest.MatcherAssert.assertThat; | ||
15 | import static org.hamcrest.Matchers.is; | ||
16 | import static org.hamcrest.Matchers.nullValue; | ||
17 | import static org.junit.jupiter.api.Assertions.assertAll; | ||
18 | |||
19 | public final class QueryAssertions { | ||
20 | private QueryAssertions() { | ||
21 | throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); | ||
22 | } | ||
23 | |||
24 | public static <T> void assertNullableResults(Map<Tuple, Optional<T>> expected, ResultSet<T> resultSet) { | ||
25 | var nullableValuesMap = new LinkedHashMap<Tuple, T>(expected.size()); | ||
26 | for (var entry : expected.entrySet()) { | ||
27 | nullableValuesMap.put(entry.getKey(), entry.getValue().orElse(null)); | ||
28 | } | ||
29 | assertResults(nullableValuesMap, resultSet); | ||
30 | } | ||
31 | |||
32 | public static <T> void assertResults(Map<Tuple, T> expected, ResultSet<T> resultSet) { | ||
33 | var defaultValue = resultSet.getCanonicalQuery().defaultValue(); | ||
34 | var filteredExpected = new LinkedHashMap<Tuple, T>(); | ||
35 | var executables = new ArrayList<Executable>(); | ||
36 | for (var entry : expected.entrySet()) { | ||
37 | var key = entry.getKey(); | ||
38 | var value = entry.getValue(); | ||
39 | if (!Objects.equals(value, defaultValue)) { | ||
40 | filteredExpected.put(key, value); | ||
41 | } | ||
42 | executables.add(() -> assertThat("value for key " + key,resultSet.get(key), is(value))); | ||
43 | } | ||
44 | executables.add(() -> assertThat("results size", resultSet.size(), is(filteredExpected.size()))); | ||
45 | |||
46 | var actual = new LinkedHashMap<Tuple, T>(); | ||
47 | var cursor = resultSet.getAll(); | ||
48 | while (cursor.move()) { | ||
49 | var key = cursor.getKey(); | ||
50 | var previous = actual.put(key, cursor.getValue()); | ||
51 | assertThat("duplicate value for key " + key, previous, nullValue()); | ||
52 | } | ||
53 | executables.add(() -> assertThat("results cursor", actual, is(filteredExpected))); | ||
54 | |||
55 | assertAll(executables); | ||
56 | } | ||
57 | } | ||
diff --git a/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/tests/QueryBackendHint.java b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/tests/QueryBackendHint.java new file mode 100644 index 00000000..f9d5b219 --- /dev/null +++ b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/tests/QueryBackendHint.java | |||
@@ -0,0 +1,27 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.tests; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.backend.QueryEvaluationHint; | ||
9 | |||
10 | /** | ||
11 | * Overrides {@link QueryEvaluationHint#toString()} for pretty names in parametric test names. | ||
12 | */ | ||
13 | class QueryBackendHint extends QueryEvaluationHint { | ||
14 | public QueryBackendHint(BackendRequirement backendRequirementType) { | ||
15 | super(null, backendRequirementType); | ||
16 | } | ||
17 | |||
18 | @Override | ||
19 | public String toString() { | ||
20 | return switch (getQueryBackendRequirementType()) { | ||
21 | case UNSPECIFIED -> "default"; | ||
22 | case DEFAULT_CACHING -> "incremental"; | ||
23 | case DEFAULT_SEARCH -> "localSearch"; | ||
24 | default -> throw new IllegalStateException("Unknown BackendRequirement"); | ||
25 | }; | ||
26 | } | ||
27 | } | ||
diff --git a/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/tests/QueryEngineTest.java b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/tests/QueryEngineTest.java new file mode 100644 index 00000000..a5cc7e9c --- /dev/null +++ b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/tests/QueryEngineTest.java | |||
@@ -0,0 +1,21 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.tests; | ||
7 | |||
8 | import org.junit.jupiter.params.ParameterizedTest; | ||
9 | import org.junit.jupiter.params.provider.ArgumentsSource; | ||
10 | |||
11 | import java.lang.annotation.ElementType; | ||
12 | import java.lang.annotation.Retention; | ||
13 | import java.lang.annotation.RetentionPolicy; | ||
14 | import java.lang.annotation.Target; | ||
15 | |||
16 | @ParameterizedTest(name = "backend = {0}") | ||
17 | @ArgumentsSource(QueryEvaluationHintSource.class) | ||
18 | @Target(ElementType.METHOD) | ||
19 | @Retention(RetentionPolicy.RUNTIME) | ||
20 | public @interface QueryEngineTest { | ||
21 | } | ||
diff --git a/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/tests/QueryEvaluationHintSource.java b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/tests/QueryEvaluationHintSource.java new file mode 100644 index 00000000..6503ff2f --- /dev/null +++ b/subprojects/store-query-interpreter/src/test/java/tools/refinery/store/query/interpreter/tests/QueryEvaluationHintSource.java | |||
@@ -0,0 +1,24 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.query.interpreter.tests; | ||
7 | |||
8 | import tools.refinery.interpreter.matchers.backend.QueryEvaluationHint; | ||
9 | import org.junit.jupiter.api.extension.ExtensionContext; | ||
10 | import org.junit.jupiter.params.provider.Arguments; | ||
11 | import org.junit.jupiter.params.provider.ArgumentsProvider; | ||
12 | |||
13 | import java.util.stream.Stream; | ||
14 | |||
15 | public class QueryEvaluationHintSource implements ArgumentsProvider { | ||
16 | @Override | ||
17 | public Stream<? extends Arguments> provideArguments(ExtensionContext context) { | ||
18 | return Stream.of( | ||
19 | Arguments.of(new QueryBackendHint(QueryEvaluationHint.BackendRequirement.UNSPECIFIED)), | ||
20 | Arguments.of(new QueryBackendHint(QueryEvaluationHint.BackendRequirement.DEFAULT_CACHING)), | ||
21 | Arguments.of(new QueryBackendHint(QueryEvaluationHint.BackendRequirement.DEFAULT_SEARCH)) | ||
22 | ); | ||
23 | } | ||
24 | } | ||