Skip to content

Commit

Permalink
Merge branch 'release/0.19.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
RKrahl committed Jul 20, 2021
2 parents c85d5c4 + 781adec commit 8c882a3
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 60 deletions.
28 changes: 28 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@ Changelog
=========


0.19.0 (2021-07-20)
~~~~~~~~~~~~~~~~~~~

New features
------------

+ `#85`_: add an argument `join_specs` to the constructor of class
:class:`icat.query.Query` and a corresponding method
:meth:`icat.query.Query.setJoinSpecs` to override the join
specification to be used in the created query for selected related
objects.

Bug fixes and minor changes
---------------------------

+ `#83`_, `#84`_: enable ordering on one to many relationships in
class :class:`icat.query.Query`.

+ `#84`_: Add warning classes
:exc:`icat.exception.QueryOneToManyOrderWarning` and
:exc:`icat.exception.QueryWarning`, the latter being a common base
class for warnings emitted during creation of a query.

.. _#83: https://github.com/icatproject/python-icat/issues/83
.. _#84: https://github.com/icatproject/python-icat/pull/84
.. _#85: https://github.com/icatproject/python-icat/pull/85


0.18.1 (2021-04-13)
~~~~~~~~~~~~~~~~~~~

Expand Down
12 changes: 11 additions & 1 deletion doc/src/exception.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,18 @@ Exceptions raised by python-icat
:members:
:show-inheritance:

.. autoexception:: icat.exception.QueryWarning
:members:
:show-inheritance:

.. autoexception:: icat.exception.QueryNullableOrderWarning
:members:
:show-inheritance:

.. autoexception:: icat.exception.QueryOneToManyOrderWarning
:members:
:show-inheritance:

.. autoexception:: icat.exception.ClientVersionWarning
:members:
:show-inheritance:
Expand Down Expand Up @@ -178,7 +186,9 @@ The class hierarchy for the exceptions is::
+-- IDSResponseError
+-- GenealogyError
+-- Warning
+-- QueryNullableOrderWarning
+-- QueryWarning
| +-- QueryNullableOrderWarning
| +-- QueryOneToManyOrderWarning
+-- ClientVersionWarning
+-- DeprecationWarning
+-- ICATDeprecationWarning
Expand Down
6 changes: 2 additions & 4 deletions icat/dump_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,9 @@ def getAuthQueries(client):
return [ Query(client, "User", order=True),
Query(client, "Grouping", order=True,
includes={"userGroups", "userGroups.user"}),
Query(client, "Rule", order=["what", "id"],
conditions={"grouping": "IS NULL"}),
Query(client, "Rule", order=["grouping.name", "what", "id"],
conditions={"grouping": "IS NOT NULL"},
includes={"grouping"}),
includes={"grouping"},
join_specs={"grouping": "LEFT JOIN"}),
Query(client, "PublicStep", order=True) ]

def getStaticQueries(client):
Expand Down
28 changes: 24 additions & 4 deletions icat/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
# icat.config
'ConfigError',
# icat.query
'QueryNullableOrderWarning',
'QueryWarning', 'QueryNullableOrderWarning', 'QueryOneToManyOrderWarning',
# icat.client, icat.entity
'ClientVersionWarning', 'ICATDeprecationWarning',
'EntityTypeError', 'VersionMethodError', 'SearchResultError',
Expand Down Expand Up @@ -305,14 +305,34 @@ class ConfigError(_BaseException):

# ================ Exceptions raised in icat.query =================

class QueryNullableOrderWarning(Warning):
"""Warn about using a nullable relation for ordering.
class QueryWarning(Warning):
"""Warning while building a query.
.. versionadded:: 0.19.0
"""
pass

class QueryNullableOrderWarning(QueryWarning):
"""Warn about using a nullable many to one relation for ordering.
.. versionchanged:: 0.19.0
Inherit from :exc:`QueryWarning`.
"""
def __init__(self, attr):
msg = ("ordering on a nullable relation implicitly "
msg = ("ordering on a nullable many to one relation implicitly "
"adds a '%s IS NOT NULL' condition." % attr)
super(QueryNullableOrderWarning, self).__init__(msg)

class QueryOneToManyOrderWarning(QueryWarning):
"""Warn about using a one to many relation for ordering.
.. versionadded:: 0.19.0
"""
def __init__(self, attr):
msg = ("ordering on a one to many relation %s may surprisingly "
"affect the search result." % attr)
super(QueryOneToManyOrderWarning, self).__init__(msg)


# ======== Exceptions raised in icat.client and icat.entity ========

Expand Down
89 changes: 77 additions & 12 deletions icat/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
"""

from warnings import warn
try:
# Python 3.3 and newer
from collections.abc import Mapping
except ImportError:
# Python 2
from collections import Mapping
import icat.entity
from icat.exception import *

Expand Down Expand Up @@ -45,6 +51,16 @@
:meth:`icat.query.Query.setAggregate` method.
"""

jpql_join_specs = frozenset([
"JOIN",
"INNER JOIN",
"LEFT JOIN",
"LEFT OUTER JOIN",
])
"""Allowed values for the `join_specs` argument to the
:meth:`icat.query.Query.setJoinSpecs` method.
"""

# ========================== class Query =============================

class Query(object):
Expand Down Expand Up @@ -75,22 +91,30 @@ class Query(object):
:param limit: a tuple (skip, count) to be used in the LIMIT
clause. See the :meth:`~icat.query.Query.setLimit` method for
details.
:param join_specs: a mapping to override the join specification
for selected related objects. See the
:meth:`~icat.query.Query.setJoinSpecs` method for details.
:param attribute: alias for `attributes`, retained for
compatibility. Deprecated, use `attributes` instead.
:raise TypeError: if `entity` is not a valid entity type or if
both `attributes` and `attribute` are provided.
:raise TypeError: if `entity` is not a valid entity type, if both
`attributes` and `attribute` are provided, or if any of the
keyword arguments have an invalid type, see the corresponding
method for details.
:raise ValueError: if any of the keyword arguments is not valid,
see the corresponding method for details.
.. versionchanged:: 0.18.0
add support for queries requesting a list of attributes rather
then a single one. Consequently, the keyword argument
`attribute` has been renamed to `attributes` (in the plural).
.. versionchanged:: 0.19.0
add the `join_specs` argument.
"""

def __init__(self, client, entity,
attributes=None, aggregate=None, order=None,
conditions=None, includes=None, limit=None, attribute=None):
conditions=None, includes=None, limit=None,
join_specs=None, attribute=None):
"""Initialize the query.
"""

Expand Down Expand Up @@ -123,6 +147,7 @@ def __init__(self, client, entity,
self.addConditions(conditions)
self.includes = set()
self.addIncludes(includes)
self.setJoinSpecs(join_specs)
self.setOrder(order)
self.setLimit(limit)
self._init = None
Expand Down Expand Up @@ -251,6 +276,39 @@ def setAggregate(self, function):
else:
self.aggregate = None

def setJoinSpecs(self, join_specs):
"""Override the join specifications.
:param join_specs: a mapping of related object names to join
specifications. Allowed values are "JOIN", "INNER JOIN",
"LEFT JOIN", and "LEFT OUTER JOIN". Any entry in this
mapping overrides how this particular related object is to
be joined. The default for any relation not included in
the mapping is "JOIN". A special value of :const:`None`
for `join_specs` is equivalent to the empty mapping.
:type join_specs: :class:`dict`
:raise TypeError: if `join_specs` is not a mapping.
:raise ValueError: if any key in `join_specs` is not a name of
a related object or if any value is not in the allowed
set.
.. versionadded:: 0.19.0
"""
if join_specs:
if not isinstance(join_specs, Mapping):
raise TypeError("join_specs must be a mapping")
for obj, js in join_specs.items():
for (pattr, attrInfo, rclass) in self._attrpath(obj):
pass
if rclass is None:
raise ValueError("%s.%s is not a related object"
% (self.entity.BeanName, obj))
if js not in jpql_join_specs:
raise ValueError("invalid join specification %s" % js)
self.join_specs = join_specs
else:
self.join_specs = dict()

def setOrder(self, order):
"""Set the order to build the ORDER BY clause from.
Expand All @@ -262,8 +320,12 @@ def setOrder(self, order):
name and an order direction, the latter being either "ASC"
or "DESC" for ascending or descending order respectively.
:type order: iterable or :class:`bool`
:raise ValueError: if `order` contains invalid attributes that
either do not exist or contain one to many relationships.
:raise ValueError: if any attribute in `order` is not valid.
.. versionchanged:: 0.19.0
allow one to many relationships in `order`. Emit a
:exc:`~icat.exception.QueryOneToManyOrderWarning` rather
then raising a :exc:`ValueError` in this case.
"""
if order is True:

Expand All @@ -285,15 +347,17 @@ def setOrder(self, order):

for (pattr, attrInfo, rclass) in self._attrpath(obj):
if attrInfo.relType == "ONE":
if (not attrInfo.notNullable and
pattr not in self.conditions):
if (not attrInfo.notNullable and
pattr not in self.conditions and
pattr not in self.join_specs):
sl = 3 if self._init else 2
warn(QueryNullableOrderWarning(pattr),
warn(QueryNullableOrderWarning(pattr),
stacklevel=sl)
elif attrInfo.relType == "MANY":
raise ValueError("Cannot use one to many relationship "
"in '%s' to order %s."
% (obj, self.entity.BeanName))
if (pattr not in self.join_specs):
sl = 3 if self._init else 2
warn(QueryOneToManyOrderWarning(pattr),
stacklevel=sl)

if rclass is None:
# obj is an attribute, use it right away.
Expand Down Expand Up @@ -424,7 +488,8 @@ def __str__(self):
base = "SELECT %s FROM %s o" % (res, self.entity.BeanName)
joins = ""
for obj in sorted(subst.keys()):
joins += " JOIN %s" % self._dosubst(obj, subst)
js = self.join_specs.get(obj, "JOIN")
joins += " %s %s" % (js, self._dosubst(obj, subst))
if self.conditions:
conds = []
for a in sorted(self.conditions.keys()):
Expand Down
Loading

0 comments on commit 8c882a3

Please sign in to comment.