Ejemplo n.º 1
0
    def apply_to_statement(self, query: QueryObject, target_Model: SAModelOrAlias, stmt: sa.sql.Select) -> sa.sql.Select:
        # Prepare the filter expression
        cursor = self.cursor_value

        if cursor is None:
            filter_expression = True
            limit = self.limit
        else:
            # Make sure the columns are still the same
            if set(cursor.cols) != query.sort.names:
                raise exc.QueryObjectError('You cannot adjust "sort" fields while using cursor-based pagination.')

            # Filter
            op = {'>': operator.gt, '<': operator.lt}[cursor.op]
            filter_expression = op(
                sa.tuple_(*(
                    resolve_column_by_name(field.name, target_Model, where='skip')
                    for field in query.sort.fields
                )),
                cursor.val
            )
            limit = cursor.limit

        if limit is None:
            return stmt

        # Paginate
        # We will always load one more row to check if there's a next page
        if SA_14:
            return stmt.filter(filter_expression).limit(limit + 1)
        else:
            return stmt.where(filter_expression).limit(limit + 1)
Ejemplo n.º 2
0
    def apply_to_statement(self, stmt: sa.sql.Select) -> sa.sql.Select:
        """ Modify the Select statement: add the WHERE clause """
        # Compile the conditions
        conditions = (self._compile_condition(condition)
                      for condition in self.query.filter.conditions)

        # Add the WHERE clause
        if SA_13:
            stmt = stmt.where(sa.and_(*conditions))
        else:
            stmt = stmt.filter(*conditions)

        # Done
        return stmt
Ejemplo n.º 3
0
    def prepare_query(self, q: sa.sql.Select) -> sa.sql.Select:
        """ Prepare the statement for loading: add columns to select, add filter condition

        Args:
            q: SELECT statement prepared by QueryExecutor.statement().
               It has no columns yet, but has a select_from(self.target_model), unaliased.
               NOTE: we never alias the target model: the one we're loading. It would've made things too complicated.
        """
        # Use SelectInLoader
        # self.query_info: primary key columns, the IN expression, etc
        # self._parent_alias: used with JOINed relationships where our table has to be joined to an alias of the parent table
        query_info = self.loader._query_info
        parent_alias = self.loader._parent_alias if query_info.load_with_join else NotImplemented
        effective_entity = self.target_model

        # [ADDED] Adapt pk_cols
        # [o] pk_cols = query_info.pk_cols
        # [o] in_expr = query_info.in_expr
        pk_cols = query_info.pk_cols
        in_expr = query_info.in_expr

        # [o] if not query_info.load_with_join:
        if not query_info.load_with_join:
            # [o] if effective_entity.is_aliased_class:
            # [o]     pk_cols = [ effective_entity._adapt_element(col) for col in pk_cols ]
            # [o]     in_expr = effective_entity._adapt_element(in_expr)
            adapter = SimpleColumnsAdapter(self.target_model)
            pk_cols = adapter.replace_many(pk_cols)
            in_expr = adapter.replace(in_expr)

        # [o] bundle_ent = orm_util.Bundle("pk", *pk_cols)
        # [o] entity_sql = effective_entity.__clause_element__()
        # [o] q = Select._create_raw_select(
        # [o]     _raw_columns=[bundle_sql, entity_sql],
        # [o]     _label_style=LABEL_STYLE_TABLENAME_PLUS_COL,
        # [CUSTOMIZED]
        if not query_info.load_with_join:
            q = add_columns(q, pk_cols)  # [CUSTOMIZED]
        else:
            # NOTE: we cannot always add our FK columns: when `load_with_join` is used, these columns
            # may actually refer to columns from a M2M table with conflicting names!
            # Example:
            #   SELECT articles.id, tags.id
            #   FROM articles JOIN ... JOIN tags
            # So we have to rename them. We use "table.column" aliases because this horrible "." makes it clear
            # it's not just another column
            # label_prefix = self.source_model.__table__.name + '.'
            self.fk_label_prefix = self.source_model.__tablename__ + '.'  # type: ignore[union-attr]
            q = add_columns(q, [  # [CUSTOMIZED]
                col.label(self.fk_label_prefix + col.key)
                for col in pk_cols
            ])

        # Effective entity
        # This is the class that we select from
        # [o] if not query_info.load_with_join:
        # [o]     q = q.select_from(effective_entity)
        # [o] else:
        # [o]     q = q.select_from(self._parent_alias).join(...)
        # [CUSTOMIZED]
        if not query_info.load_with_join:
            q = q.select_from(self.target_model)
        else:
            if SA_13:
                q = q.select_from(
                    sa.orm.join(parent_alias, self.target_model, onclause=getattr(parent_alias, self.key).of_type(self.target_model))
                )
            else:
                q = q.select_from(parent_alias).join(
                    getattr(parent_alias, self.key).of_type(self.target_model)
                )

        # [o] q = q.filter(in_expr.in_(sql.bindparam("primary_keys")))
        if SA_13:
            q = q.where(in_expr.in_(sa.sql.bindparam("primary_keys", expanding=True)))
        else:
            q = q.filter(in_expr.in_(sa.sql.bindparam("primary_keys")))

        return q
Ejemplo n.º 4
0
    def _apply_window_over_foreign_key_pagination(
            self, stmt: sa.sql.Select, *,
            fk_columns: list[SAAttribute]) -> sa.sql.Select:
        """ Instead of the usual limit, use a window function over the given columns.

        This method is used with the selectin-load loading strategy to load a limited number of related
        items per every primary entity. Instead of using LIMIT, we will group rows over `fk_columns`,
        and impose a limit per group.

        This is achieved using a Window Function:

            SELECT *, row_number() OVER(PARTITION BY author_id) AS group_row_n
            FROM articles
            WHERE group_row_n < 10

            This will result in the following table:

            id  |   author_id   |   group_row_n
            ------------------------------------
            1       1               1
            2       1               2
            3       2               1
            4       2               2
            5       2               3
            6       3               1
            7       3               2
        """
        skip, limit = self.skip, self.limit

        # Apply it only when there's a limit
        if not skip and not limit:
            return stmt

        # First, add a row counter
        adapter = SimpleColumnsAdapter(self.target_Model)
        row_counter_col = (
            sa.func.row_number().over(
                # Groups are partitioned by self._window_over_columns,
                partition_by=adapter.replace_many(
                    fk_columns),  # type: ignore[arg-type]
                # We have to apply the same ordering from the outside query;
                # otherwise, the numbering will be undetermined
                order_by=adapter.replace_many(
                    get_sort_fields_with_direction(
                        self.query.sort,
                        self.target_Model))  # type: ignore[arg-type]
            )
            # give it a name that we can use later
            .label('__group_row_n'))
        stmt = add_columns(stmt, [row_counter_col])

        # Wrap ourselves into a subquery.
        # This is necessary because Postgres does not let you reference SELECT aliases in the WHERE clause.
        # Reason: WHERE clause is executed before SELECT
        if SA_14:
            subquery = (
                # Taken from: Query.from_self()
                stmt.correlate(None).subquery()._anonymous_fromclause(
                )  # type: ignore[attr-defined]
            )
        else:
            subquery = (stmt.correlate(None).alias())

        stmt = sa.select([
            column for column in subquery.c if column.key !=
            '__group_row_n'  # skip this column. We don't need it.
        ]).select_from(subquery)

        # Apply the LIMIT condition using row numbers
        # These two statements simulate skip/limit using window functions
        if skip:
            if SA_14:
                stmt = stmt.filter(
                    sa.sql.literal_column('__group_row_n') > skip)
            else:
                stmt = stmt.where(
                    sa.sql.literal_column('__group_row_n') > skip)
        if limit:
            if SA_14:
                stmt = stmt.filter(
                    sa.sql.literal_column('__group_row_n') <= (
                        (skip or 0) + limit))
            else:
                stmt = stmt.where(
                    sa.sql.literal_column('__group_row_n') <= (
                        (skip or 0) + limit))

        # Done
        return stmt