"""Replace dashboard_widgets.position with a Grafana-style grid (x/y/w/h) Revision ID: 0021_dashboard_widget_grid Revises: 0020_ansible_run_results Create Date: 2026-06-17 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa revision: str = "0021_dashboard_widget_grid" down_revision: Union[str, None] = "0020_ansible_run_results" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # New grid columns (server_default so existing rows are populated; the ORM # supplies these on insert thereafter). 12-col grid, default widget 4x4. op.add_column("dashboard_widgets", sa.Column("grid_x", sa.Integer, nullable=False, server_default="0")) op.add_column("dashboard_widgets", sa.Column("grid_y", sa.Integer, nullable=False, server_default="0")) op.add_column("dashboard_widgets", sa.Column("grid_w", sa.Integer, nullable=False, server_default="4")) op.add_column("dashboard_widgets", sa.Column("grid_h", sa.Integer, nullable=False, server_default="4")) # Backfill from the old `position` order into a 3-up grid, reproducing the # previous masonry layout: 3 widgets per row, each 4 cols wide x 4 cells tall. op.execute( "UPDATE dashboard_widgets " "SET grid_x = (position % 3) * 4, grid_y = (position / 3) * 4" ) op.drop_column("dashboard_widgets", "position") def downgrade() -> None: op.add_column("dashboard_widgets", sa.Column("position", sa.Integer, nullable=False, server_default="0")) # Reconstruct a linear order from the grid (reading order: top-to-bottom, # then left-to-right within a row). op.execute( "UPDATE dashboard_widgets SET position = grid_y * 3 + (grid_x / 4)" ) op.drop_column("dashboard_widgets", "grid_h") op.drop_column("dashboard_widgets", "grid_w") op.drop_column("dashboard_widgets", "grid_y") op.drop_column("dashboard_widgets", "grid_x")