Skip to content

Commit

Permalink
feat: implement query checking
Browse files Browse the repository at this point in the history
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; also a run_query_check=False flag is
added to the QueryConstructor class so that QueryConstructor could
also be used as a standalone class with query checking enabled.

Closes #116.
  • Loading branch information
lu-pl committed Jan 23, 2025
1 parent b1ea529 commit 2177a12
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 2 deletions.
3 changes: 2 additions & 1 deletion rdfproxy/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from rdfproxy.mapper import _ModelBindingsMapper
from rdfproxy.sparql_strategies import HttpxStrategy, SPARQLStrategy
from rdfproxy.utils._types import _TModelInstance
from rdfproxy.utils.checkers.query_checker import check_query
from rdfproxy.utils.models import Page, QueryParameters


Expand Down Expand Up @@ -40,7 +41,7 @@ def __init__(
sparql_strategy: type[SPARQLStrategy] = HttpxStrategy,
) -> None:
self._target = target
self._query = query
self._query = check_query(query)
self._model = model

self.sparql_strategy = sparql_strategy(self._target)
Expand Down
4 changes: 3 additions & 1 deletion rdfproxy/constructor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from rdfproxy.utils._types import _TModelInstance
from rdfproxy.utils.checkers.query_checker import check_query
from rdfproxy.utils.models import QueryParameters
from rdfproxy.utils.sparql_utils import (
add_solution_modifier,
Expand Down Expand Up @@ -27,8 +28,9 @@ def __init__(
query: str,
query_parameters: QueryParameters,
model: type[_TModelInstance],
run_query_check: bool = False,
) -> None:
self.query = query
self.query = check_query(query) if run_query_check else query
self.query_parameters = query_parameters
self.model = model

Expand Down
15 changes: 15 additions & 0 deletions rdfproxy/utils/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,18 @@ class InvalidGroupingKeyException(Exception):

class QueryConstructionException(Exception):
"""Exception for indicating failed SPARQL query construction."""


class UnsupportedQueryException(Exception):
"""Exception for indicating that a given SPARQL query is not supported."""


class QueryParseException(Exception):
"""Exception for indicating that a given SPARQL query raised a parse error.
This exception is intended to wrap and re-raise all exceptions
raised from parsing a SPARQL query with RDFLib's parseQuery function.
parseQuery raises a pyparsing.exceptions.ParseException,
which would require to introduce pyparsing as a dependency just for testing.
"""
35 changes: 35 additions & 0 deletions rdfproxy/utils/checkers/query_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Functionality for performing checks on SPARQL queries."""

import logging
from typing import TypeVar

from rdflib.plugins.sparql.parser import parseQuery
from rdfproxy.utils._exceptions import QueryParseException, UnsupportedQueryException
from rdfproxy.utils.utils import compose_left


logger = logging.getLogger(__name__)

_TQuery = TypeVar("_TQuery", bound=str)


def _check_select_query(query: _TQuery) -> _TQuery:
"""Check if a query is parsable and a SELECT query."""
logger.debug("Running parsable SELECT check on '%s'", query)

try:
parsed = parseQuery(query)
except Exception as e:
raise QueryParseException from e
else:
_, query_type = parsed
if query_type.name != "SelectQuery":
raise UnsupportedQueryException("Only SELECT queries are applicable.")

return query


def check_query(query: _TQuery) -> _TQuery:
"""Run a series of checks on a query."""
logger.debug("Running query checks on '%s'", query)
return compose_left(_check_select_query)(query)

0 comments on commit 2177a12

Please sign in to comment.