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
+
1
16
import { map , find , uniqueId , partial , groupBy } from 'lodash' ;
2
17
import * as _ from 'lodash' ;
3
18
@@ -9,9 +24,12 @@ import Node from '../common-rdf/node';
9
24
10
25
import queryTemplate from './query-template' ;
11
26
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.
15
33
16
34
interface TaggedExpression {
17
35
tag : 'expression' ;
@@ -25,18 +43,29 @@ interface TaggedPattern {
25
43
26
44
type TaggedSyntax = TaggedExpression | TaggedPattern ;
27
45
46
+ /**
47
+ * Return type of `_.groupBy(TaggedSyntax[], 'tag')`, useful for and/or.
48
+ */
28
49
interface Branches {
29
50
expression ?: TaggedExpression [ ] ;
30
51
pattern ?: TaggedPattern [ ] ;
31
52
} ;
32
53
54
+ interface nsTable {
55
+ [ abbreviation : string ] : string ;
56
+ }
57
+
33
58
const defaultNs = {
34
59
rdfs : rdfs ( ) ,
35
60
owl : owl ( ) ,
36
61
readit : readit ( ) ,
37
62
item : item ( ) ,
38
63
} ;
39
64
65
+ /**
66
+ * Serialize an IRI either as `<http://full.url>` or as `ns:short`, depending
67
+ * on available namespaces.
68
+ */
40
69
export function serializeIri ( iri : string , ns : nsTable ) : string {
41
70
let short = '' ;
42
71
find ( ns , function ( namespace , abbreviation ) {
@@ -50,6 +79,9 @@ export function serializeIri(iri: string, ns: nsTable): string {
50
79
return short || `<${ iri } >` ;
51
80
}
52
81
82
+ /**
83
+ * Serialize a SPARQL-supported literal with a type in the `xsd` namespace.
84
+ */
53
85
export function serializeLiteral (
54
86
literal : string , datatype : string , ns : nsTable
55
87
) : string {
@@ -59,13 +91,23 @@ export function serializeLiteral(
59
91
case xsd . string :
60
92
return `"${ literal } "` ;
61
93
}
94
+ // Assume number since that's the only other type SPARQL supports.
62
95
return literal ;
63
96
}
64
97
65
98
function nextVariable ( ) : string {
66
99
return uniqueId ( '?x' ) ;
67
100
}
68
101
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
+ */
69
111
export function serializePredicate ( predicate : Node , ns : nsTable ) : string {
70
112
const inverse = predicate . get ( owl . inverseOf ) as Node [ ] ;
71
113
if ( inverse && inverse . length ) return `^${ serializeIri ( inverse [ 0 ] . id , ns ) } ` ;
@@ -84,6 +126,12 @@ function tagPattern(pattern: string): TaggedPattern {
84
126
return { tag : 'pattern' , pattern } ;
85
127
}
86
128
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
+ */
87
135
export function serializeExpression ( filter : Model , args : string [ ] ) : TaggedExpression {
88
136
const func = filter . get ( 'function' ) || '' ;
89
137
const op = filter . get ( 'operator' ) ;
@@ -95,6 +143,7 @@ function patternAsExpression({ pattern }: TaggedPattern): TaggedExpression {
95
143
return tagExpression ( `EXISTS {\n${ pattern } }` ) ;
96
144
}
97
145
146
+ // Below, we generate two helpers for `combineAnd` and `combineOr`.
98
147
function joinTagged < K extends keyof Branches > ( key : K ) {
99
148
return function ( constituents : Branches [ K ] , glue : string ) : string {
100
149
return map ( constituents , key ) . join ( glue ) ;
@@ -103,6 +152,11 @@ function joinTagged<K extends keyof Branches>(key: K) {
103
152
const joinE = joinTagged ( 'expression' ) ;
104
153
const joinP = joinTagged ( 'pattern' ) ;
105
154
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
+ */
106
160
export function combineAnd ( { expression, pattern } : Branches ) : TaggedSyntax {
107
161
let exp = expression ? `${ joinE ( expression , ' && ' ) } ` : '' ;
108
162
if ( expression && expression . length > 1 ) exp = `(${ exp } )` ;
@@ -114,6 +168,11 @@ export function combineAnd({ expression, pattern }: Branches): TaggedSyntax {
114
168
return tagPattern ( pat ) ;
115
169
}
116
170
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
+ */
117
176
export function combineOr ( { expression, pattern } : Branches ) : TaggedSyntax {
118
177
if ( expression ) {
119
178
const patExp = expression . concat (
@@ -125,11 +184,17 @@ export function combineOr({ expression, pattern }: Branches): TaggedSyntax {
125
184
return tagPattern ( `{\n${ joinP ( pattern , '} UNION {\n' ) } }\n` ) ;
126
185
}
127
186
187
+ // Lookup table to save an `if`/`else` down the line.
128
188
const combine = {
129
189
and : combineAnd ,
130
190
or : combineOr ,
131
191
} ;
132
192
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
+ */
133
198
function negate ( syntax : TaggedSyntax ) : TaggedExpression {
134
199
return tagExpression (
135
200
syntax . tag === 'expression' ?
@@ -138,6 +203,12 @@ function negate(syntax: TaggedSyntax): TaggedExpression {
138
203
) ;
139
204
}
140
205
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
+ */
141
212
export function serializeChain (
142
213
entry : Model , variableIn : string , ns : nsTable , index : number = 0
143
214
) : TaggedSyntax {
@@ -146,11 +217,18 @@ export function serializeChain(
146
217
const predicates : Node [ ] = [ ] ;
147
218
const args : string [ ] = [ ] ;
148
219
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.
149
226
let tail : TaggedSyntax ;
150
227
while ( index < chain . length ) {
151
228
const model = chain . at ( index ) ;
152
229
const scheme = model . get ( 'scheme' ) ;
153
230
if ( scheme === 'logic' ) {
231
+ // Logic, recurse.
154
232
const branches = model . get ( 'branches' ) ;
155
233
const action = model . get ( 'action' ) ;
156
234
if ( branches ) {
@@ -161,6 +239,7 @@ export function serializeChain(
161
239
break ;
162
240
}
163
241
} else if ( scheme === 'filter' ) {
242
+ // Filter, build expression as `tail`.
164
243
const value = model . get ( 'value' ) ;
165
244
const datatype = model . get ( 'range' ) . at ( 0 ) . id ;
166
245
args . push ( variableOut ) ;
@@ -171,9 +250,16 @@ export function serializeChain(
171
250
) ;
172
251
tail = serializeExpression ( model . get ( 'filter' ) , args ) ;
173
252
} else if ( model . get ( 'traversal' ) ) {
253
+ // Add another property to traverse.
174
254
predicates . push ( model . get ( 'selection' ) ) ;
175
255
if ( variableOut === variableIn ) variableOut = nextVariable ( ) ;
176
256
}
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'`.
177
263
++ index ;
178
264
}
179
265
if ( ! tail ) throw new RangeError (
@@ -188,6 +274,10 @@ export function serializeChain(
188
274
) ;
189
275
}
190
276
277
+ /**
278
+ * Recursion helper for `serializeChain` that accumulates the results when
279
+ * branching out over multiple subchains by and/or.
280
+ */
191
281
function serializeBranchout (
192
282
branches : Collection , action : string , variableIn : string , ns : nsTable
193
283
) : TaggedSyntax {
@@ -199,8 +289,14 @@ function serializeBranchout(
199
289
return combine [ action ] ( segments ) ;
200
290
}
201
291
292
+ // Callback used with `_.map` below to convert `nsTable` to the format that
293
+ // `../sparql/query-templates/preamble-template.hbs` requires.
202
294
const explodeNs = ( prefix , label ) => ( { label, prefix } ) ;
203
295
296
+ /**
297
+ * Convert the data model into a complete `CONSTRUCT` query including prefix
298
+ * headers.
299
+ */
204
300
export default function modelToQuery (
205
301
entry : Model , ns : nsTable = defaultNs
206
302
) : string {
0 commit comments