-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The change introduces a check_query callable which runs an extensible compose pipeline of query checkers. Note regarding QueryParseException: This custom exception is intended to be a thin wrapper around a pyparsing ParseException that RDFLib raises. This avoids introducing pyparsing as a dependency just to be able to test against this exception. I feel like RDFLib should not raise a pyparsing exception but provide a thin wrapper itself. See RDFLib/rdflib#3057. The check_query function runs in SPARQLModelAdapter to enable fast failures on inapplicable queries. Note that this somewhat couples QueryConstructor to SPARQLModelAdapter; QueryConstructor should be marked private for this reason. Closes #116. Closes #126.
- Loading branch information
Showing
4 changed files
with
116 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
"""Functionality for performing checks on SPARQL queries.""" | ||
|
||
import logging | ||
from typing import TypeVar | ||
from typing import no_type_check | ||
|
||
from rdflib.plugins.sparql.parser import parseQuery | ||
from rdflib.plugins.sparql.parserutils import CompValue | ||
from rdfproxy.utils._exceptions import QueryParseException, UnsupportedQueryException | ||
from rdfproxy.utils.utils import compose_left | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
_TQuery = TypeVar("_TQuery", bound=str) | ||
|
||
|
||
def _parse_query(query: _TQuery) -> tuple[_TQuery, CompValue]: | ||
"""Check if a query is parsable. | ||
This is meant to be the first component in check_query. | ||
""" | ||
logger.debug("Running parse check.") | ||
|
||
try: | ||
_parsed = parseQuery(query) | ||
except Exception as e: | ||
raise QueryParseException(e) from e | ||
else: | ||
_, parse_object = _parsed | ||
return query, parse_object | ||
|
||
|
||
def _check_select_query(data: tuple[_TQuery, CompValue]): | ||
"""Check if a SPARQL query is a SELECT query. | ||
This is meant to run as a component in check_query. | ||
""" | ||
logger.debug("Running SELECT query check.") | ||
|
||
_, parse_object = data | ||
if parse_object.name != "SelectQuery": | ||
raise UnsupportedQueryException("Only SELECT queries are applicable.") | ||
return data | ||
|
||
|
||
def _check_solution_modifiers( | ||
data: tuple[_TQuery, CompValue], | ||
) -> tuple[_TQuery, CompValue]: | ||
"""Check if a SPARQL query has a solution modifier. | ||
This is meant to run as a component in check_query. | ||
""" | ||
logger.debug("Running solution modifier check.") | ||
|
||
_, parse_object = data | ||
|
||
def _has_modifier(): | ||
for mod_name in ["limitoffset", "groupby", "having", "orderby"]: | ||
if (mod := getattr(parse_object, mod_name)) is not None: | ||
return mod | ||
return False | ||
|
||
if mod := _has_modifier(): | ||
logger.critical("Detected solution modifier '%s' in outer query.", mod) | ||
raise UnsupportedQueryException( | ||
"Solution modifiers for top-level queries are currently not supported." | ||
) | ||
|
||
return data | ||
|
||
|
||
def _get_query_string(data: tuple[_TQuery, CompValue]) -> _TQuery: | ||
"""Return the query from a query/parse_object tuple. | ||
This is meant to be the last component in check_query. | ||
""" | ||
query, _ = data | ||
return query | ||
|
||
|
||
@no_type_check | ||
def check_query(query: _TQuery) -> _TQuery: | ||
"""Check a SPARQL query by running a compose pipeline of checks. | ||
The pipeline expects a SPARQL query string and | ||
will return that string if all checks pass. | ||
_parse_query is meant to be the first component | ||
and _get_query_string is meant to be the last component. | ||
""" | ||
logger.debug("Running query check pipeline on '%s'", query) | ||
return compose_left( | ||
_parse_query, | ||
_check_select_query, | ||
_check_solution_modifiers, | ||
_get_query_string, | ||
)(query) |