Skip to content

Partition#

Bases: SsasRefreshRecord

Partitions are a child of Tables. They contain the Power Query code.

These are the physical segments of the table that contain the data. They cannot be edited within the Power BI Desktop UI, but can be edited in the Tabular Editor or other tools (like this one!). Data refreshes occur on the Partition-level.

SSAS spec: Microsoft

Source code in pbi_core/ssas/model_tables/partition/partition.py
class Partition(SsasRefreshRecord):
    """Partitions are a child of Tables. They contain the Power Query code.

    These are the physical segments of the table that contain the data.
    They cannot be edited within the Power BI Desktop UI, but can be edited in the
    Tabular Editor or other tools (like this one!). Data refreshes occur on the Partition-level.

    SSAS spec: [Microsoft](https://learn.microsoft.com/en-us/openspecs/sql_server_protocols/ms-ssas-t/81badb81-31a8-482b-ae16-5fc9d8291d9e)
    """

    _default_refresh_type = RefreshType.FULL

    data_view: DataView
    data_source_id: int | None = None
    description: str | None = None
    error_message: str | None = None
    expression_source_id: int | None = None
    m_attributes: str | None = None
    mode: PartitionMode
    name: str
    partition_storage_id: int
    query_definition: str
    query_group_id: int | None = None
    range_granularity: int
    retain_data_till_force_calculate: bool
    state: DataState
    system_flags: int
    table_id: int
    type: PartitionType

    modified_time: datetime.datetime
    refreshed_time: datetime.datetime

    _commands: RefreshCommands = PrivateAttr(default_factory=lambda: SsasCommands.partition)

    def expression_ast(self) -> dax.Expression | pq.Expression | None:
        if self.type == PartitionType.CALCULATED:
            ret = pq.to_ast(self.query_definition)
            if ret is None:
                msg = "Failed to parse DAX expression from partition query definition"
                raise ValueError(msg)
        elif self.type == PartitionType.M:
            ret = dax.to_ast(self.query_definition)
            if ret is None:
                msg = "Failed to parse M expression from partition query definition"
                raise ValueError(msg)
        else:
            logger.warning("Attempted to get AST of non-M/DAX partition", partition=self.name, type=self.type)
            return None

    def expression_source(self) -> "Expression | None":
        if self.expression_source_id is None:
            return None
        return self.tabular_model.expressions.find(self.expression_source_id)

    def is_system_table(self) -> bool:
        return bool(self.system_flags >> 1 % 2)

    def is_from_calculated_table(self) -> bool:
        return bool(self.system_flags % 2)

    def query_group(self) -> "QueryGroup | None":
        try:
            return self.tabular_model.query_groups.find({"id": self.table_id})
        except RowNotFoundError:
            return None

    def table(self) -> "Table":
        return self.tabular_model.tables.find({"id": self.table_id})

    def get_lineage(self, lineage_type: Literal["children", "parents"]) -> LineageNode:
        if lineage_type == "children":
            return LineageNode(self, lineage_type)
        parent_nodes: list[SsasTable | None] = [self.table(), self.query_group()]
        parent_lineage: list[LineageNode] = [c.get_lineage(lineage_type) for c in parent_nodes if c is not None]
        return LineageNode(self, lineage_type, parent_lineage)

    def remove_columns(self, dropped_columns: "Iterable[Column | str | None]") -> BeautifulSoup:
        def pq_escape(x: str) -> str:
            """Beginning of column escaping for power query."""
            return x.replace('"', '""')

        """Adds a Table.RemoveColumns statement to the end of the Partition's PowerQuery.

        This means the upon refresh, the columns will not be included in the table
        """
        new_dropped_columns: list[str] = []
        for col in dropped_columns:
            if isinstance(col, Column):
                # Tables have a column named "RowNumber-<UUID>" that cannot be removed in the PowerQuery
                if col._column_type() != "CALC_COLUMN" and not col.is_key:
                    assert col.explicit_name is not None, "Column must have an explicit name to be dropped"
                    new_dropped_columns.append(col.explicit_name)
            elif isinstance(col, str):
                new_dropped_columns.append(col)

        # TODO: create a powerquery parser to do this robustly
        new_dropped_columns = [pq_escape(x) for x in new_dropped_columns]
        logger.info("Updating partition to drop columns", table=self.table().name, columns=new_dropped_columns)
        lines = self.query_definition.split("\n")
        final_table_name = lines[-1].strip()
        setup = "\n".join(lines[:-2])

        prior_updates = setup.count("pbi_update")  # used to keep statement variables unique when applied multiple times
        new_final_table_name = f"pbi_update{prior_updates}"

        cols = ", ".join(f'"{x}"' for x in new_dropped_columns)
        setup += f",\n    {new_final_table_name} = Table.RemoveColumns({final_table_name}, {{{cols}}})"
        setup += f"\nin\n    {new_final_table_name}"
        self.query_definition = setup
        return self.alter()