Skip to content

Commit

Permalink
Add LoadOption for chaining load options to Query load methods.
Browse files Browse the repository at this point in the history
  • Loading branch information
dgilland committed Aug 30, 2014
1 parent 65cc14d commit 6dce510
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 22 deletions.
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ Changelog
=========


v1.1.0 (2014-08-30)
-------------------

- Add ``query.LoadOption`` to support nesting load options when calling the ``query.Query`` load methods: ``join_eager``, ``outerjoin_eager``, ``joinedload``, ``immediateload``, ``lazyload``, ``noload``, and ``subqueryload``.


v1.0.0 (2014-08-25)
-------------------

Expand Down
174 changes: 152 additions & 22 deletions alchy/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def __getattr__(self, attr):
'QueryModel',
'QueryProperty',
'Pagination',
'LoadOption'
]


Expand Down Expand Up @@ -63,56 +64,152 @@ def _join_eager(self, keys, outerjoin, **kargs):
``contains_eager()``.
"""
alias = kargs.pop('alias', None)
options = kargs.pop('options', None)

key = keys[0]
path_keys = keys[1:]

join_args = ([alias, key] if alias else [key]) + list(path_keys)

opt = orm.contains_eager(key, alias=alias)
load = orm.contains_eager(key, alias=alias)

for k in path_keys:
opt = opt.contains_eager(k)
load = load.contains_eager(k)

if options:
apply_load_options(load, options)

join = self.outerjoin if outerjoin else self.join

return join(*join_args).options(opt)
return join(*join_args).options(load)

def join_eager(self, *keys, **kargs):
"""Apply ``join`` + ``self.options(contains_eager())``."""
"""Apply ``join`` + ``self.options(contains_eager())``.
Args:
keys (mixed): Either string or column references to join
path(s)
Keyword Args:
alias: Join alias
options (list): A list of :class:`LoadOption` to apply to the
overall load strategy, i.e., each :class:`LoadOption` will be
chained at the end of the load.
"""
return self._join_eager(keys, False, **kargs)

def outerjoin_eager(self, *keys, **kargs):
"""Apply ``outerjoin`` + ``self.options(contains_eager())``."""
"""Apply ``outerjoin`` + ``self.options(contains_eager())``.
Args:
keys (mixed): Either string keys or column references to join
path(s)
Keyword Args:
alias: Join alias
options (list): A list of :class:`LoadOption` to apply to the
overall load strategy, i.e., each :class:`LoadOption` will be
chained at the end of the load.
"""
return self._join_eager(keys, True, **kargs)

def _join_load(self, keys, load_type, **kargs):
def _join_load(self, keys, load_strategy, **kargs):
"""Helper method for returning load strategies."""
opt = getattr(orm, load_type)(keys[0], **kargs)
options = kargs.pop('options', None)

load = getattr(orm, load_strategy)(keys[0], **kargs)

for k in keys[1:]:
opt = getattr(opt, load_type)(k)
load = getattr(load, load_strategy)(k)

if options:
load = apply_load_options(load, options)

return self.options(opt)
return self.options(load)

def joinedload(self, *keys, **kargs):
"""Apply ``joinedload()`` to `keys`."""
"""Apply ``joinedload()`` to `keys`.
Args:
keys (mixed): Either string or column references to join
path(s)
Keyword Args:
options (list): A list of :class:`LoadOption` to apply to the
overall load strategy, i.e., each :class:`LoadOption` will be
chained at the end of the load.
Note:
Additional keyword args will be passed to initial load creation.
"""
return self._join_load(keys, 'joinedload', **kargs)

def immediateload(self, *keys, **kargs):
"""Apply ``immediateload()`` to `keys`."""
"""Apply ``immediateload()`` to `keys`.
Args:
keys (mixed): Either string or column references to join
path(s)
Keyword Args:
options (list): A list of :class:`LoadOption` to apply to the
overall load strategy, i.e., each :class:`LoadOption` will be
chained at the end of the load.
Note:
Additional keyword args will be passed to initial load creation.
"""
return self._join_load(keys, 'immediateload', **kargs)

def lazyload(self, *keys, **kargs):
"""Apply ``lazyload()`` to `keys`."""
"""Apply ``lazyload()`` to `keys`.
Args:
keys (mixed): Either string or column references to join
path(s)
Keyword Args:
options (list): A list of :class:`LoadOption` to apply to the
overall load strategy, i.e., each :class:`LoadOption` will be
chained at the end of the load.
Note:
Additional keyword args will be passed to initial load creation.
"""
return self._join_load(keys, 'lazyload', **kargs)

def noload(self, *keys, **kargs):
"""Apply ``noload()`` to `keys`."""
"""Apply ``noload()`` to `keys`.
Args:
keys (mixed): Either string or column references to join
path(s)
Keyword Args:
options (list): A list of :class:`LoadOption` to apply to the
overall load strategy, i.e., each :class:`LoadOption` will be
chained at the end of the load.
Note:
Additional keyword args will be passed to initial load creation.
"""
return self._join_load(keys, 'noload', **kargs)

def subqueryload(self, *keys, **kargs):
"""Apply ``subqueryload()`` to `keys`."""
"""Apply ``subqueryload()`` to `keys`.
Args:
keys (mixed): Either string or column references to join
path(s)
Keyword Args:
options (list): A list of :class:`LoadOption` to apply to the
overall load strategy, i.e., each :class:`LoadOption` will be
chained at the end of the load.
Note:
Additional keyword args will be passed to initial load creation.
"""
return self._join_load(keys, 'subqueryload', **kargs)

def load_only(self, *columns):
Expand All @@ -122,19 +219,17 @@ def load_only(self, *columns):

def defer(self, *columns):
"""Apply ``defer()`` to query."""
obj, columns = get_load_options(*columns)
opts = obj
load, columns = get_load_options(*columns)
for column in columns:
opts = opts.defer(column)
return self.options(opts)
load = load.defer(column)
return self.options(load)

def undefer(self, *columns):
"""Apply ``undefer()`` to query."""
obj, columns = get_load_options(*columns)
opts = obj
load, columns = get_load_options(*columns)
for column in columns:
opts = opts.undefer(column)
return self.options(opts)
load = load.undefer(column)
return self.options(load)

def undefer_group(self, *names):
"""Apply ``undefer_group()`` to query."""
Expand Down Expand Up @@ -442,6 +537,29 @@ def next(self, error_out=False):
return self.query.paginate(self.page + 1, self.per_page, error_out)


class LoadOption(object):
"""Chained load option to apply to a load strategy when calling
:class:`Query` load methods.
Example usage: ::
qry = (db.session.query(Product)
.join_eager('category',
options=[LoadOption('noload', 'images')]))
This would result in the ``noload`` option being chained to the eager
option for ``Product.category`` and is equilvalent to: ::
qry = (db.session.query(Product)
.join('category')
.options(contains_eager('category').noload('images')))
"""
def __init__(self, strategy, *args, **kargs):
self.strategy = strategy
self.args = args
self.kargs = kargs


def get_load_options(*columns):
"""Helper method that attempts to extract a sqlalchemy object from
`columns[0]` and return remaining columns to apply to a query load method.
Expand All @@ -461,6 +579,18 @@ def get_load_options(*columns):
return (obj, columns)


def apply_load_options(load, options):
"""Apply load `options` to base `load` object.
The `options` dict should be indexed by the load method name. It's values
should
"""
for load_option in options:
load = getattr(load, load_option.strategy)(*load_option.args,
**load_option.kargs)

return load

def base_columns_from_subquery(subquery):
"""Return non-aliased, base columns from subquery."""
# base_columns is a set so we need to cast to list.
Expand Down
67 changes: 67 additions & 0 deletions tests/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,18 @@ def test_join_eager(self):
'it should join eager on multiple model entities'
)

self.assertEqual(
str((self.db.query(Foo)
.join_eager(
'bars',
options=[query.LoadOption('contains_eager', 'bazs')])
)),
str((self.db.query(Foo)
.join('bars')
.options(orm.contains_eager('bars').contains_eager('bazs')))),
'it should join eager using options'
)

def test_join_eager_with_alias(self):
bar_alias = orm.aliased(Bar)

Expand Down Expand Up @@ -275,6 +287,18 @@ def test_outerouterjoin_eager(self):
'it should outerjoin eager on multiple model entities'
)

self.assertEqual(
str((self.db.query(Foo)
.outerjoin_eager(
'bars',
options=[query.LoadOption('contains_eager', 'bazs')])
)),
str((self.db.query(Foo)
.outerjoin('bars')
.options(orm.contains_eager('bars').contains_eager('bazs')))),
'it should join eager using options'
)

def test_outerouterjoin_eager_with_alias(self):
bar_alias = orm.aliased(Bar)

Expand Down Expand Up @@ -317,6 +341,15 @@ def test_joinedload(self):
.options(orm.joinedload(Foo.bars).joinedload(Bar.bazs))))
)

self.assertEqual(
str((self.db.query(Foo)
.joinedload('bars',
options=[query.LoadOption('joinedload', 'bazs')]))
),
str((self.db.query(Foo)
.options(orm.joinedload('bars').joinedload('bazs'))))
)

def test_immediateload(self):
self.assertEqual(
str(self.db.query(Foo).immediateload('bars')),
Expand All @@ -340,6 +373,15 @@ def test_immediateload(self):
.options(orm.immediateload(Foo.bars).immediateload(Bar.bazs))))
)

self.assertEqual(
str((self.db.query(Foo)
.immediateload('bars',
options=[query.LoadOption('immediateload',
'bazs')]))),
str((self.db.query(Foo)
.options(orm.immediateload('bars').immediateload('bazs'))))
)

def test_lazyload(self):
self.assertEqual(
str(self.db.query(Foo).lazyload('bars')),
Expand All @@ -363,6 +405,14 @@ def test_lazyload(self):
.options(orm.lazyload(Foo.bars).lazyload(Bar.bazs))))
)

self.assertEqual(
str((self.db.query(Foo)
.lazyload('bars',
options=[query.LoadOption('lazyload', 'bazs')]))),
str((self.db.query(Foo)
.options(orm.lazyload('bars').lazyload('bazs'))))
)

def test_noload(self):
self.assertEqual(
str(self.db.query(Foo).noload('bars')),
Expand All @@ -385,6 +435,13 @@ def test_noload(self):
.options(orm.noload(Foo.bars).noload(Bar.bazs))))
)

self.assertEqual(
str((self.db.query(Foo)
.noload('bars',
options=[query.LoadOption('noload', 'bazs')]))),
str(self.db.query(Foo).options(orm.noload('bars').noload('bazs')))
)

def test_subqueryload(self):
self.assertEqual(
str(self.db.query(Foo).subqueryload('bars')),
Expand All @@ -408,6 +465,16 @@ def test_subqueryload(self):
.options(orm.subqueryload(Foo.bars).subqueryload(Bar.bazs))))
)

self.assertEqual(
str((self.db.query(Foo)
.subqueryload(
'bars',
options=[query.LoadOption('subqueryload', 'bazs')])
)),
str((self.db.query(Foo)
.options(orm.subqueryload('bars').subqueryload('bazs'))))
)

def test_load_only_with_string_args(self):
# with load_only()
item = self.db.query(Foo).load_only('_id', 'string').first().__dict__
Expand Down

0 comments on commit 6dce510

Please sign in to comment.