Skip to content

Commit dd5fb55

Browse files
committed
docs: add docs section on SPARQL result conversion
1 parent f9997ba commit dd5fb55

1 file changed

Lines changed: 123 additions & 1 deletion

File tree

docs/rdflib_integration.md

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,129 @@
11
# RDFLib Integration
22

33
## SPARQL Result Conversion
4-
[todo]
4+
5+
As mentioned in the [README](../README.md), `SPARQLWrapper` query methods feature a `convert: bool` flag for conversion of SPARQL responses to Python result representations.
6+
7+
E.g. if the `convert` parameter is set to `True`, `SPARQLWrapper.query` returns
8+
9+
- a `list` of Python dictionaries with dict-values cast to RDFLib objects for `SELECT` queries
10+
- a Python `bool` for `ASK` queries
11+
- an `rdflib.Graph` instance for `CONSTRUCT` and `DESCRIBE` queries.
12+
13+
Not that currently only JSON is supported as a response format for `convert=True` on `SELECT` and `ASK` query results.
14+
15+
16+
### JSON Response Conversion of SELECT Query Results
17+
18+
While for graph result conversion (`CONSTRUCT` and `DESCRIBE` queries), the requested `response_format` is simply passed to `rdflib.Graph.parse` along with the response content,
19+
the `SELECT` query converter logic in `sparqlx` processes response JSON adhering to the [SPARQL 1.2 Query Results JSON Format](https://www.w3.org/TR/sparql12-results-json/) and generates a Python mapping with RDFLib-casted JSON result values;
20+
i.e. the conversion of `SELECT` query results returns a `list` of mappings with values of type `sparqlx.types.SPARQLResultBindingValue` (a union type `rdflib.URIRef | rdflib.BNode | sparqlx.types.LiteralToPython`).
21+
22+
23+
An important divergence from the SPARQL 1.2 Query Results JSON Format lies in how `sparqlx` handles `UNDEF` SPARQL bindings in `SELECT` result conversions.
24+
While `UNDEF` SPARQL bindings are actually *dropped entirely* for the respective result row in the standard Query Results JSON, `sparqlx` inspects the projection and guarantees stable result shapes by assigning `None` for `UNDEF` bindings.
25+
26+
Consider the following `VALUES` example:
27+
28+
```sparql
29+
select ?x
30+
where {
31+
values ?x {1 undef}
32+
}
33+
```
34+
35+
The raw JSON response for this query will be:
36+
37+
```json
38+
{
39+
"head": {
40+
"vars": [
41+
"x"
42+
]
43+
},
44+
"results": {
45+
"bindings": [
46+
{
47+
"x": {
48+
"datatype": "http://www.w3.org/2001/XMLSchema#integer",
49+
"type": "literal",
50+
"value": "1"
51+
}
52+
},
53+
{}
54+
]
55+
}
56+
}
57+
```
58+
59+
Note that the second binding map is empty.
60+
61+
62+
The same query with `SPARQLWrapper.query` and `convert=True` produces the following result of type `sparqlx.types.SPARQLResultBinding`:
63+
64+
```python
65+
[{'x': 1}, {'x': None}]
66+
```
67+
68+
This normalization of result shapes according to the query projection is convenient for SPARQL result processing e.g. with Pydantic models or the like.
69+
70+
71+
The explication of missing data as `None`-bindings in `sparqlx` has subtle consequences that one should be aware of though.
72+
73+
The following lists a few noteworthy cases.
74+
75+
#### Cases
76+
77+
- Query: `select ?s ?o where {values ?o {1} optional {?s ?p ?o}}`
78+
- convert=False: `{'head': {'vars': ['s', 'o']}, 'results': {'bindings': [{'o': {'datatype': 'xsd:integer', ..., 'value': '1'}}]}}`
79+
- convert=True: `[{'s': None, 'o': 1}]`
80+
- Comment: Here, optional`?s` will never be bound. The SPARQL JSON response omits unbound variables while `sparqlx` conversion guarantees stable result shapes according to the projection.
81+
82+
- Query: `select ?dne where {}`
83+
- convert=False: `{'head': {'vars': ['dne']}, 'results': {'bindings': [{}]}}`
84+
- convert=True: `[{ 'dne': None }]`
85+
- Comment: See the [5.2.1 Empty Group Pattern](https://www.w3.org/TR/sparql12-query/#emptyGroupPattern) in SPARQL 1.2 Spec. `?dne` is in the projection and therefore present in the converted result.
86+
87+
- Query: `select * where {}`
88+
- convert=False: `{'head': {'vars': []}, 'results': {'bindings': [{}]}}`
89+
- convert=True: `[{}]`
90+
- Comment: The query has an Empty Group Pattern (see above). Since there are no variables in the projection, the converted result holds just an empty mapping.
91+
92+
- Query: `select * where {?s ?p ?o} limit 0`
93+
- convert=False: `{'head': {'vars': ['s', 'p', 'o']}, 'results': {'bindings': []}}`
94+
- convert=True: `[]`
95+
- Comment: Variables in the projection, but no bindings; so nothing to do for the bindings converter.
96+
97+
- Query: `select * where {minus {?s ?p ?o}}`
98+
- convert=False: `{'head': {'vars': ['s', 'p', 'o']}, 'results': {'bindings': [{}]}}`
99+
- convert=True: `[{'s': None, 'p': None, 'o': None}]`
100+
- Comment: The MINUS set operation performed on an Empty Group Pattern; this behaves as the Empty Group Pattern, but with variables in the projection, which has consequences for the `sparqlx` conversion.
101+
102+
- Query: `select * where {filter not exists {?s ?p ?o}}` (non-empty graph)
103+
- convert=False: `{'head': {'vars': []}, 'results': {'bindings': []}}`
104+
- convert=True: `[]`
105+
- Comment: Given a non-empty graph, the existential condition in the filter clause is not satisfiable and the query therefore has no solution.
106+
107+
- Query: `select * where {filter not exists {?s ?p ?o}}` (empty graph)
108+
- convert=False: `{'head': {'vars': ['s', 'p', 'o']}, 'results': {'bindings': [{}]}}`
109+
- convert=True: `[{'s': None, 'p': None, 'o': None}]`
110+
- Comment: Given an empty graph, the existential condition in the filter is always satisfiable and therefore results in an identity join on the Empty Group Pattern.
111+
112+
For the differences between FILTER NOT EXISTS and MINUS see [8. Negation](https://www.w3.org/TR/sparql12-query/#negation) in the SPARQL 1.2 Spec.
113+
114+
115+
Generally interesting case: Join Identity/Annihilator
116+
117+
- Query: `select * where {filter(true)}`
118+
- convert=False: `{'head': {'vars': []}, 'results': {'bindings': [{}]}}`
119+
- convert=True: `[{}]`
120+
- Comment: The filter clause is always satisfiable, so this is essentially Join Identity.
121+
122+
- Query: `select * where {filter(false)}`
123+
- convert=False: `{'head': {'vars': []}, 'results': {'bindings': []}}`
124+
- convert=True: `[]`
125+
- Comment: The filter clause is never satisfiable, so this annihilates any join.
126+
5127

6128
## `rdflib.Graph` Targets
7129

0 commit comments

Comments
 (0)