Skip to content

Commit 295a152

Browse files
committed
Document modelToQuery (#455)
1 parent 9da69ae commit 295a152

File tree

1 file changed

+99
-3
lines changed

1 file changed

+99
-3
lines changed

frontend/src/semantic-search/modelToQuery.ts

+99-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
/**
2+
* SemanticSearchView and its subviews work with an underlying data model,
3+
* which is rich enough to represent both the recursive user interface and the
4+
* recursive SPARQL query that is generated from it. In the current module, we
5+
* implement the SPARQL code generation from this common data model.
6+
*
7+
* We generate a useful, logically complete subset of SPARQL that lends itself
8+
* well to being constructed from the user interface. Specifically, we use
9+
* inverse and sequence property paths, `EXISTS` and `NOT EXISTS`, `UNION`,
10+
* pattern concatenation, and a handful of functions and operators.
11+
*
12+
* The default export of the module, `modelToQuery`, is the real interface.
13+
* Some other functions are exported as well, but only for unittesting purposes.
14+
*/
15+
116
import { map, find, uniqueId, partial, groupBy } from 'lodash';
217
import * as _ from 'lodash';
318

@@ -9,9 +24,12 @@ import Node from '../common-rdf/node';
924

1025
import queryTemplate from './query-template';
1126

12-
interface nsTable {
13-
[abbreviation: string]: string;
14-
}
27+
// SPARQL is essentially a bi-modal query language: query conditions can be
28+
// either patterns (consisting of triples and delimited by curly braces) or
29+
// expressions (built up from function calls and operators). Expressions can be
30+
// injected into patterns using `FILTER` statements. Conversely, patterns can be
31+
// nested inside expressions using the `EXISTS` function. The next three types
32+
// enable us to distinguish between the two modes.
1533

1634
interface TaggedExpression {
1735
tag: 'expression';
@@ -25,18 +43,29 @@ interface TaggedPattern {
2543

2644
type TaggedSyntax = TaggedExpression | TaggedPattern;
2745

46+
/**
47+
* Return type of `_.groupBy(TaggedSyntax[], 'tag')`, useful for and/or.
48+
*/
2849
interface Branches {
2950
expression?: TaggedExpression[];
3051
pattern?: TaggedPattern[];
3152
};
3253

54+
interface nsTable {
55+
[abbreviation: string]: string;
56+
}
57+
3358
const defaultNs = {
3459
rdfs: rdfs(),
3560
owl: owl(),
3661
readit: readit(),
3762
item: item(),
3863
};
3964

65+
/**
66+
* Serialize an IRI either as `<http://full.url>` or as `ns:short`, depending
67+
* on available namespaces.
68+
*/
4069
export function serializeIri(iri: string, ns: nsTable): string {
4170
let short = '';
4271
find(ns, function(namespace, abbreviation) {
@@ -50,6 +79,9 @@ export function serializeIri(iri: string, ns: nsTable): string {
5079
return short || `<${iri}>`;
5180
}
5281

82+
/**
83+
* Serialize a SPARQL-supported literal with a type in the `xsd` namespace.
84+
*/
5385
export function serializeLiteral(
5486
literal: string, datatype: string, ns: nsTable
5587
): string {
@@ -59,13 +91,23 @@ export function serializeLiteral(
5991
case xsd.string:
6092
return `"${literal}"`;
6193
}
94+
// Assume number since that's the only other type SPARQL supports.
6295
return literal;
6396
}
6497

6598
function nextVariable(): string {
6699
return uniqueId('?x');
67100
}
68101

102+
/**
103+
* In the context of a predicate path, we write the IRI of an inverse property
104+
* as `^direct`. This is safer than always writing an inverse property as
105+
* itself, firstly because this is also how related items are saved to the
106+
* backend, and secondly because our frontend reasoner synthetically generates
107+
* some "pretend" inverse properties from their direct counterparts when no
108+
* existing inverse is found. The latter mechanism is implemented in
109+
* `../utilities/relation-utilities.ts`.
110+
*/
69111
export function serializePredicate(predicate: Node, ns: nsTable): string {
70112
const inverse = predicate.get(owl.inverseOf) as Node[];
71113
if (inverse && inverse.length) return `^${serializeIri(inverse[0].id, ns)}`;
@@ -84,6 +126,12 @@ function tagPattern(pattern: string): TaggedPattern {
84126
return { tag: 'pattern', pattern };
85127
}
86128

129+
/**
130+
* Serialize one atomic building block of an expression: either a function call
131+
* with only literal or variable arguments, or a binary operator expression
132+
* with likewise operands. See `./dropdown-constants.ts` for the possible
133+
* filter models.
134+
*/
87135
export function serializeExpression(filter: Model, args: string[]): TaggedExpression {
88136
const func = filter.get('function') || '';
89137
const op = filter.get('operator');
@@ -95,6 +143,7 @@ function patternAsExpression({ pattern }: TaggedPattern): TaggedExpression {
95143
return tagExpression(`EXISTS {\n${pattern}}`);
96144
}
97145

146+
// Below, we generate two helpers for `combineAnd` and `combineOr`.
98147
function joinTagged<K extends keyof Branches>(key: K) {
99148
return function(constituents: Branches[K], glue: string): string {
100149
return map(constituents, key).join(glue);
@@ -103,6 +152,11 @@ function joinTagged<K extends keyof Branches>(key: K) {
103152
const joinE = joinTagged('expression');
104153
const joinP = joinTagged('pattern');
105154

155+
/**
156+
* Apply logical AND to combine a bunch of SPARQL snippets which have already
157+
* been pre-grouped by mode (expression/pattern). The resulting SPARQL snippet
158+
* may be either a pattern or an expression, depending on what went in.
159+
*/
106160
export function combineAnd({ expression, pattern }: Branches): TaggedSyntax {
107161
let exp = expression ? `${joinE(expression, ' && ')}` : '';
108162
if (expression && expression.length > 1) exp = `(${exp})`;
@@ -114,6 +168,11 @@ export function combineAnd({ expression, pattern }: Branches): TaggedSyntax {
114168
return tagPattern(pat);
115169
}
116170

171+
/**
172+
* Apply logical OR to combine a bunch of SPARQL snippets which have already
173+
* been pre-grouped by mode (expression/pattern). The resulting SPARQL snippet
174+
* may be either a pattern or an expression, depending on what went in.
175+
*/
117176
export function combineOr({ expression, pattern }: Branches): TaggedSyntax {
118177
if (expression) {
119178
const patExp = expression.concat(
@@ -125,11 +184,17 @@ export function combineOr({ expression, pattern }: Branches): TaggedSyntax {
125184
return tagPattern(`{\n${joinP(pattern, '} UNION {\n')}}\n`);
126185
}
127186

187+
// Lookup table to save an `if`/`else` down the line.
128188
const combine = {
129189
and: combineAnd,
130190
or: combineOr,
131191
};
132192

193+
/**
194+
* Apply logical NOT to a SPARQL snippet which may be of either mode
195+
* (expression/pattern). The result is always an expression; patterns need to
196+
* be converted to expression first because they cannot be negated directly.
197+
*/
133198
function negate(syntax: TaggedSyntax): TaggedExpression {
134199
return tagExpression(
135200
syntax.tag === 'expression' ?
@@ -138,6 +203,12 @@ function negate(syntax: TaggedSyntax): TaggedExpression {
138203
);
139204
}
140205

206+
/**
207+
* Core recursive pattern/expression builder, representing an entire chain
208+
* (row) from the UI, including any subchains that branch out from it. The
209+
* recursion is depth-first and pre-order, so that the smallest constituents
210+
* determine the mode of their containing constituents.
211+
*/
141212
export function serializeChain(
142213
entry: Model, variableIn: string, ns: nsTable, index: number = 0
143214
): TaggedSyntax {
@@ -146,11 +217,18 @@ export function serializeChain(
146217
const predicates: Node[] = [];
147218
const args: string[] = [];
148219
let variableOut: string = variableIn;
220+
// Conceptually, a chain consists of zero or more property traversals,
221+
// optionally recursing on logical operators. Eventually, chains always
222+
// terminate with a filter. `tail` will contain the SPARQL syntax that
223+
// results either from recursion or termination. The purpose of the loop
224+
// below is to accumulate the properties to traverse until the `tail` is
225+
// found.
149226
let tail: TaggedSyntax;
150227
while (index < chain.length) {
151228
const model = chain.at(index);
152229
const scheme = model.get('scheme');
153230
if (scheme === 'logic') {
231+
// Logic, recurse.
154232
const branches = model.get('branches');
155233
const action = model.get('action');
156234
if (branches) {
@@ -161,6 +239,7 @@ export function serializeChain(
161239
break;
162240
}
163241
} else if (scheme === 'filter') {
242+
// Filter, build expression as `tail`.
164243
const value = model.get('value');
165244
const datatype = model.get('range').at(0).id;
166245
args.push(variableOut);
@@ -171,9 +250,16 @@ export function serializeChain(
171250
);
172251
tail = serializeExpression(model.get('filter'), args);
173252
} else if (model.get('traversal')) {
253+
// Add another property to traverse.
174254
predicates.push(model.get('selection'));
175255
if (variableOut === variableIn) variableOut = nextVariable();
176256
}
257+
// You may wonder why there is no final `else` clause. The reason is
258+
// that some models in a chain only serve a purpose for the UI. Those
259+
// are (1) the models corresponding to an "expect type" choice, which
260+
// are currently left implicit in the SPARQL query (since the traversed
261+
// properties already imply a type), and (2) filter selections, which
262+
// are always followed by another model with `scheme === 'filter'`.
177263
++index;
178264
}
179265
if (!tail) throw new RangeError(
@@ -188,6 +274,10 @@ export function serializeChain(
188274
);
189275
}
190276

277+
/**
278+
* Recursion helper for `serializeChain` that accumulates the results when
279+
* branching out over multiple subchains by and/or.
280+
*/
191281
function serializeBranchout(
192282
branches: Collection, action: string, variableIn: string, ns: nsTable
193283
): TaggedSyntax {
@@ -199,8 +289,14 @@ function serializeBranchout(
199289
return combine[action](segments);
200290
}
201291

292+
// Callback used with `_.map` below to convert `nsTable` to the format that
293+
// `../sparql/query-templates/preamble-template.hbs` requires.
202294
const explodeNs = (prefix, label) => ({ label, prefix });
203295

296+
/**
297+
* Convert the data model into a complete `CONSTRUCT` query including prefix
298+
* headers.
299+
*/
204300
export default function modelToQuery(
205301
entry: Model, ns: nsTable = defaultNs
206302
): string {

0 commit comments

Comments
 (0)