sqlobject.sresults — SQLObject 3.9.1 documentation

# Source code for sqlobject.sresults

    from . import dbconnection
    from . import sqlbuilder
    from .compat import string_type

    __all__ = ['SelectResults']

    class SelectResults(object):
        IterationClass = dbconnection.Iteration

        def __init__(self, sourceClass, clause, clauseTables=None,
                     **ops):
            self.sourceClass = sourceClass
            if clause is None or isinstance(clause, str) and clause == 'all':
                clause = sqlbuilder.SQLTrueClause
            if not isinstance(clause, sqlbuilder.SQLExpression):
                clause = sqlbuilder.SQLConstant(clause)
            self.clause = clause
            self.ops = ops
            if ops.get('orderBy', sqlbuilder.NoDefault) is sqlbuilder.NoDefault:
                ops['orderBy'] = sourceClass.sqlmeta.defaultOrder
            orderBy = ops['orderBy']
            if isinstance(orderBy, (tuple, list)):
                orderBy = list(map(self._mungeOrderBy, orderBy))
            else:
                orderBy = self._mungeOrderBy(orderBy)
            ops['dbOrderBy'] = orderBy
            if 'connection' in ops and ops['connection'] is None:
                del ops['connection']
            if ops.get('limit', None):
                assert not ops.get('start', None) and not ops.get('end', None), \
                    "'limit' cannot be used with 'start' or 'end'"
                ops["start"] = 0
                ops["end"] = ops.pop("limit")

            tablesSet = sqlbuilder.tablesUsedSet(self.clause,
                                                 self._getConnection().dbName)
            if clauseTables:
                for table in clauseTables:
                    tablesSet.add(table)
            self.clauseTables = clauseTables
            # Explicitly post-adding-in sqlmeta.table,
            # sqlbuilder.Select will handle sqlrepr'ing and dupes.
            self.tables = list(tablesSet) + [sourceClass.sqlmeta.table]

        def queryForSelect(self):
            columns = [self.sourceClass.q.id] + \
                [getattr(self.sourceClass.q, x.name)
                 for x in self.sourceClass.sqlmeta.columnList]
            query = sqlbuilder.Select(columns,
                                      where=self.clause,
                                      join=self.ops.get(
                                          'join', sqlbuilder.NoDefault),
                                      distinct=self.ops.get('distinct', False),
                                      lazyColumns=self.ops.get(
                                          'lazyColumns', False),
                                      start=self.ops.get('start', 0),
                                      end=self.ops.get('end', None),
                                      orderBy=self.ops.get(
                                          'dbOrderBy', sqlbuilder.NoDefault),
                                      reversed=self.ops.get('reversed', False),
                                      staticTables=self.tables,
                                      forUpdate=self.ops.get('forUpdate', False))
            return query

        def __repr__(self):
            return "<%s at %x>" % (self.__class__.__name__, id(self))

        def _getConnection(self):
            return self.ops.get('connection') or self.sourceClass._connection

        def __str__(self):
            conn = self._getConnection()
            return conn.queryForSelect(self)

        def _mungeOrderBy(self, orderBy):
            if isinstance(orderBy, string_type) and orderBy.startswith('-'):
                orderBy = orderBy[1:]
                desc = True
            else:
                desc = False
            if isinstance(orderBy, string_type):
                if orderBy in self.sourceClass.sqlmeta.columns:
                    val = getattr(self.sourceClass.q,
                                  self.sourceClass.sqlmeta.columns[orderBy].name)
                    if desc:
                        return sqlbuilder.DESC(val)
                    else:
                        return val
                else:
                    orderBy = sqlbuilder.SQLConstant(orderBy)
                    if desc:
                        return sqlbuilder.DESC(orderBy)
                    else:
                        return orderBy
            else:
                return orderBy

        def clone(self, **newOps):
            ops = self.ops.copy()
            ops.update(newOps)
            return self.__class__(self.sourceClass, self.clause,
                                  self.clauseTables, **ops)

        def orderBy(self, orderBy):
            return self.clone(orderBy=orderBy)

        def connection(self, conn):
            return self.clone(connection=conn)

        def limit(self, limit):
            return self[:limit]

        def lazyColumns(self, value):
            return self.clone(lazyColumns=value)

        def reversed(self):
            return self.clone(reversed=not self.ops.get('reversed', False))

        def distinct(self):
            return self.clone(distinct=True)

        def newClause(self, new_clause):
            return self.__class__(self.sourceClass, new_clause,
                                  self.clauseTables, **self.ops)

        def filter(self, filter_clause):
            if filter_clause is None:
                # None doesn't filter anything, it's just a no-op:
                return self
            clause = self.clause
            if isinstance(clause, string_type):
                clause = sqlbuilder.SQLConstant('(%s)' % clause)
            return self.newClause(sqlbuilder.AND(clause, filter_clause))

        def __getitem__(self, value):
            if isinstance(value, slice):
                assert not value.step, "Slices do not support steps"
                if not value.start and not value.stop:
                    # No need to copy, I'm immutable
                    return self

                # Negative indexes aren't handled (and everything we
                # don't handle ourselves we just create a list to
                # handle)
                if (value.start and value.start < 0) \
                   or (value.stop and value.stop < 0):
                    if value.start:
                        if value.stop:
                            return list(self)[value.start:value.stop]
                        return list(self)[value.start:]
                    return list(self)[:value.stop]

                if value.start:
                    assert value.start >= 0
                    start = self.ops.get('start', 0) + value.start
                    if value.stop is not None:
                        assert value.stop >= 0
                        if value.stop < value.start:
                            # an empty result:
                            end = start
                        else:
                            end = value.stop + self.ops.get('start', 0)
                            if self.ops.get('end', None) is not None and \
                                    self.ops['end'] < end:
                                # truncated by previous slice:
                                end = self.ops['end']
                    else:
                        end = self.ops.get('end', None)
                else:
                    start = self.ops.get('start', 0)
                    end = value.stop + start
                    if self.ops.get('end', None) is not None \
                       and self.ops['end'] < end:
                        end = self.ops['end']
                return self.clone(start=start, end=end)
            else:
                if value < 0:
                    return list(iter(self))[value]
                else:
                    start = self.ops.get('start', 0) + value
                    return list(self.clone(start=start, end=start + 1))[0]

        def __iter__(self):
            # @@: This could be optimized, using a simpler algorithm
            # since we don't have to worry about garbage collection,
            # etc., like we do with .lazyIter()
            return iter(list(self.lazyIter()))

        def lazyIter(self):
            """
            Returns an iterator that will lazily pull rows out of the
            database and return SQLObject instances
            """
            conn = self._getConnection()
            return conn.iterSelect(self)

        def accumulate(self, *expressions):
            """ Use accumulate expression(s) to select result
                using another SQL select through current
                connection.
                Return the accumulate result
            """
            conn = self._getConnection()
            exprs = []
            for expr in expressions:
                if not isinstance(expr, sqlbuilder.SQLExpression):
                    expr = sqlbuilder.SQLConstant(expr)
                exprs.append(expr)
            return conn.accumulateSelect(self, *exprs)

        def count(self):
            """ Counting elements of current select results """
            assert not self.ops.get('start') and not self.ops.get('end'), \
                "start/end/limit have no meaning with 'count'"
            assert not (self.ops.get('distinct')
                        and (self.ops.get('start') or self.ops.get('end'))), \
                "distinct-counting of sliced objects is not supported"
            if self.ops.get('distinct'):
                # Column must be specified, so we are using unique ID column.
                # COUNT(DISTINCT column) is supported by MySQL and PostgreSQL,
                # but not by SQLite. Perhaps more portable would be subquery:
                #  SELECT COUNT(*) FROM (SELECT DISTINCT id FROM table)
                count = self.accumulate(
                    'COUNT(DISTINCT %s)' % self._getConnection().sqlrepr(
                        self.sourceClass.q.id))
            else:
                count = self.accumulate('COUNT(*)')
            if self.ops.get('start'):
                count -= self.ops['start']
            if self.ops.get('end'):
                count = min(self.ops['end'] - self.ops.get('start', 0), count)
            return count

        def accumulateMany(self, *attributes):
            """ Making the expressions for count/sum/min/max/avg
                of a given select result attributes.
                `attributes` must be a list/tuple of pairs (func_name, attribute);
                `attribute` can be a column name (like 'a_column')
                or a dot-q attribute (like Table.q.aColumn)
            """
            expressions = []
            conn = self._getConnection()
            if self.ops.get('distinct'):
                distinct = 'DISTINCT '
            else:
                distinct = ''
            for func_name, attribute in attributes:
                if not isinstance(attribute, str):
                    attribute = conn.sqlrepr(attribute)
                expression = '%s(%s%s)' % (func_name, distinct, attribute)
                expressions.append(expression)
            return self.accumulate(*expressions)

        def accumulateOne(self, func_name, attribute):
            """ Making the sum/min/max/avg of a given select result attribute.
                `attribute` can be a column name (like 'a_column')
                or a dot-q attribute (like Table.q.aColumn)
            """
            return self.accumulateMany((func_name, attribute))

        def sum(self, attribute):
            return self.accumulateOne("SUM", attribute)

        def min(self, attribute):
            return self.accumulateOne("MIN", attribute)

        def avg(self, attribute):
            return self.accumulateOne("AVG", attribute)

        def max(self, attribute):
            return self.accumulateOne("MAX", attribute)

        def getOne(self, default=sqlbuilder.NoDefault):
            """
            If a query is expected to only return a single value,
            using ``.getOne()`` will return just that value.

            If not results are found, ``SQLObjectNotFound`` will be
            raised, unless you pass in a default value (like
            ``.getOne(None)``).

            If more than one result is returned,
            ``SQLObjectIntegrityError`` will be raised.
            """
            from . import main
            results = list(self)
            if not results:
                if default is sqlbuilder.NoDefault:
                    raise main.SQLObjectNotFound(
                        "No results matched the query for %s"
                        % self.sourceClass.__name__)
                return default
            if len(results) > 1:
                raise main.SQLObjectIntegrityError(
                    "More than one result returned from query: %s"
                    % results)
            return results[0]

        def throughTo(self):
            class _throughTo_getter(object):
                def __init__(self, inst):
                    self.sresult = inst

                def __getattr__(self, attr):
                    return self.sresult._throughTo(attr)
            return _throughTo_getter(self)
        throughTo = property(throughTo)

        def _throughTo(self, attr):
            otherClass = None
            orderBy = sqlbuilder.NoDefault

            ref = self.sourceClass.sqlmeta.columns.get(
                attr.endswith('ID') and attr or attr + 'ID', None)
            if ref and ref.foreignKey:
                otherClass, clause = self._throughToFK(ref)
            else:
                join = [x for x in self.sourceClass.sqlmeta.joins
                        if x.joinMethodName == attr]
                if join:
                    join = join[0]
                    orderBy = join.orderBy
                    if hasattr(join, 'otherColumn'):
                        otherClass, clause = self._throughToRelatedJoin(join)
                    else:
                        otherClass, clause = self._throughToMultipleJoin(join)

            if not otherClass:
                raise AttributeError(
                    "throughTo argument (got %s) should be "
                    "name of foreignKey or SQL*Join in %s" % (attr,
                                                              self.sourceClass))

            return otherClass.select(clause,
                                     orderBy=orderBy,
                                     connection=self._getConnection())

        def _throughToFK(self, col):
            otherClass = getattr(self.sourceClass, "_SO_class_" + col.foreignKey)
            colName = col.name
            query = self.queryForSelect().newItems([
                sqlbuilder.ColumnAS(getattr(self.sourceClass.q, colName), colName)
            ]).orderBy(None).distinct()
            query = sqlbuilder.Alias(query,
                                     "%s_%s" % (self.sourceClass.__name__,
                                                col.name))
            return otherClass, otherClass.q.id == getattr(query.q, colName)

        def _throughToMultipleJoin(self, join):
            otherClass = join.otherClass
            colName = join.soClass.sqlmeta.style.\
                dbColumnToPythonAttr(join.joinColumn)
            query = self.queryForSelect().newItems(
                [sqlbuilder.ColumnAS(self.sourceClass.q.id, 'id')]).\
                orderBy(None).distinct()
            query = sqlbuilder.Alias(query,
                                     "%s_%s" % (self.sourceClass.__name__,
                                                join.joinMethodName))
            joinColumn = getattr(otherClass.q, colName)
            return otherClass, joinColumn == query.q.id

        def _throughToRelatedJoin(self, join):
            otherClass = join.otherClass
            intTable = sqlbuilder.Table(join.intermediateTable)
            colName = join.joinColumn
            query = self.queryForSelect().newItems(
                [sqlbuilder.ColumnAS(self.sourceClass.q.id, 'id')]).\
                orderBy(None).distinct()
            query = sqlbuilder.Alias(query,
                                     "%s_%s" % (self.sourceClass.__name__,
                                                join.joinMethodName))
            clause = sqlbuilder.AND(
                otherClass.q.id == getattr(intTable, join.otherColumn),
                getattr(intTable, colName) == query.q.id)
            return otherClass, clause